diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad23e30 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +Welcome to AiiDA-Defects +======================== + +AiiDA-Defects is a plugin for the [AiiDA](http://www.aiida.net/) computational materials science framework, and provides tools and automated workflows for the study of defects in materials. + +The package is available for download from [GitHub](http://github.com/aiida-defects). + +If you use AiiDA-Defects in your work, please cite: + +*AiiDA-defects: An automated and fully reproducible workflow for the complete characterization of defect chemistry in functional materials* +[doi.org/10.48550/arXiv.2303.12465 (preprint)](https://doi.org/10.48550/arXiv.2303.12465) + +Please also remember to cite the [AiiDA paper](https://doi.org/10.1038/s41597-020-00638-4). + + +Quick Setup +=========== + +Install this package by running the following in your shell: + + $ pip install . + +This will install all of the prerequisites automatically (including for the optional docs) +in your environment, including AiiDA core, if it not already installed. + + +Getting Started +=============== + +Expample usage of the workchains is documented in the collection of Jupyter notebooks in the ``examples`` directory. + + +Acknowledgements +================ +This work is supported by the MARVEL National Centre of Competence in Research (NCCR) funded by the Swiss National Science Foundation (grant agreement ID 51NF40-182892) and by the European Union’s Horizon 2020 research and innovation program under Grant Agreement No. 824143 (European MaX Centre of Excellence “Materials design at the Exascale”) and Grant Agreement No. 814487 (INTERSECT project). We thank Chiara Ricca and Ulrich Aschauer for discussions and prototype implementation ideas. The authors also would like to thank the Swiss National Supercomputing Centre CSCS (project s1073) for providing the computational ressources and Solvay for funding this project. We thank Arsalan Akhtar, Lorenzo Bastonero, Luca Bursi, Francesco Libbi, Riccardo De Gennaro and Daniele Tomerini for useful discussions and feedback. diff --git a/README.rst b/README.rst deleted file mode 100644 index b427ef3..0000000 --- a/README.rst +++ /dev/null @@ -1,43 +0,0 @@ -Welcome to AiiDA-Defects -++++++++++++++++++++++++ - -AiiDA-Defects is a plugin for the `AiiDA `_ computational -materials science framework, and provides tools and automated workflows for the -study of defects in materials. - -The package is available for download from `GitHub `_. - -If you use AiiDA-Defects in your work, please cite: - - *paper reference (doi)* - -Please also remember to cite the `AiiDA paper `_. - - -Quick Setup -=========== - -Install this package by running the following in your shell: - - .. code-block:: bash - - $ pip install .[docs] - -This will install all of the prerequisites automatically (including for the optional docs) -in your environment, including AiiDA core, if it not already installed. -Ideally however, you should install AiiDA-Defects after installing and setting -up AiiDA core. - -To build the local docs, run: - - .. code-block:: bash - - $ cd docs/ - $ make html - -Note: You will need to have ``make`` installed on your operating system. - - -Acknowledgements -================ -This work is funded by... \ No newline at end of file diff --git a/aiida_defects/data/data.py b/aiida_defects/data/data.py new file mode 100644 index 0000000..7e7ba36 --- /dev/null +++ b/aiida_defects/data/data.py @@ -0,0 +1,522 @@ +from string import Template + +import numpy as np +import pandas as pd +from aiida.common.exceptions import ValidationError +from aiida.common.utils import prettify_labels +from aiida.orm import ArrayData +from aiida_defects.formation_energy.chemical_potential.utils import Order_point_clockwise + +class StabilityData(ArrayData): + ''' + Class to represent the stability region of a compound and all its attributes (vertice of stability region, etc.) + The visualization only works for 2D stability region, i.e. for ternary compounds. For compounds with more than 3 elements, + some chemical potentials have to be set at certain values so that a 'slice' of the stability region can be plotted. + ''' + def set_data(self, matrix_of_constraints, indices, columns, stability_vertices, compound, dependent_element, property_map=None): + self.set_array('matrix', matrix_of_constraints) + if stability_vertices.shape[1] == 3: + self.set_array('vertices', Order_point_clockwise(stability_vertices)) # vertices include the coordinate of the dependent element as well + else: + raise ValueError('The stability vertices must be an Nx3 array where N is the number of compounds in the phase diagram.' + 'The 3 columns corresponds to the chemical potentials of each each elements' + ) + self.column = columns + self.compound = compound + self.dependent_element = dependent_element + self.index = indices + self.property_map = property_map + + def get_constraints(self): + return self.get_array('matrix') + + def get_vertices(self): + return self.get_array('vertices') + + @property + def index(self): + return self.get_attribute('index') + + @index.setter + def index(self, value): + self._set_index(value) + + def _set_index(self, value): + ''' + The equation for the dependent element is the same as that of the compound, therefore we can replace the + dependent element in indices by the compound name which is useful later for plotting purpose. + 'dependent_element' and 'compound' have to be set first + ''' + idx = [l if l != self.dependent_element else self.compound for l in value] + self.set_attribute('index', idx) + + @property + def column(self): + return self.get_attribute('column') + + @column.setter + def column(self, value): + self.set_attribute('column', value) + + @property + def compound(self): + return self.get_attribute('compound') + + @compound.setter + def compound(self, value): + self.set_attribute('compound', value) + + @property + def dependent_element(self): + return self.get_attribute('dependent_element') + + @dependent_element.setter + def dependent_element(self, value): + self.set_attribute('dependent_element', value) + + @property + def property_map(self): + return self.get_attribute('property_map') + + @property_map.setter + def property_map(self, value): + self.set_attribute('property_map', value) + + def _get_stabilityplot_data(self): + ''' + Get data to plot a stability region + Make sure the data are suitable for 2D plot. TO BE DONE. + ''' + + x_axis = np.arange(-10, 0.01, 0.05) + x = [] + y = [] + + ### Lines corresponding to each constraint associated with each compound + M = self.get_constraints() + # print(pd.DataFrame(M, index=self.index, columns=self.column)) + + ### The matrix M has the shape Nx3 where N is the number of compounds and elemental phases. + ### The 1st column is the 'x axis', 2nd column 'y axis' and 3rd column is related to the formation energy + for l in M: + if l[1] == 0.0: # vertical line + x.append([l[2]/l[0]]*len(x_axis)) + y.append(x_axis) + else: + x.append(x_axis) + y.append((l[2]-l[0]*x_axis)/l[1]) + + x = np.array(x) + y = np.array(y) + vertices = self.get_vertices() + + plot_info = {} + plot_info['x'] = x + plot_info['y'] = y + plot_info['vertices'] = vertices + boundary_lines = set() + + ### Find the boundary compounds, i.e. compounds whose corresponding lines form the edge of the stability region + for vtx in plot_info['vertices']: + # Check if a vertex is on the line, i.e its cooridinates verify the equation corresponding to that line + mask = np.abs(M[:,:2]@np.reshape(vtx[:2], (2,1))[:,0] - M[:,-1]) < 1E-4 + # Check all lines that pass through the vertex vtx + idx = [i for i, _ in enumerate(mask) if _] + # Find the corresponding name of the compound associate with that lines + boundary_lines = boundary_lines.union(set([self.index[j] for j in idx])) + plot_info['boundary_lines'] = boundary_lines + + if self.property_map: + plot_info['grid'] = self.property_map['points_in_stable_region'] + plot_info['property'] = self.property_map['property'] + + return plot_info + + def _matplotlib_get_dict( + self, + main_file_name='', + comments=True, + title='', + legend_location=None, + x_max_lim=None, + x_min_lim=None, + y_max_lim=None, + y_min_lim=None, + prettify_format=None, + **kwargs + ): # pylint: disable=unused-argument + """ + Prepare the data to send to the python-matplotlib plotting script. + + :param comments: if True, print comments (if it makes sense for the given + format) + :param plot_info: a dictionary + :param setnumber_offset: an offset to be applied to all set numbers + (i.e. s0 is replaced by s[offset], s1 by s[offset+1], etc.) + :param color_number: the color number for lines, symbols, error bars + and filling (should be less than the parameter MAX_NUM_AGR_COLORS + defined below) + :param title: the title + :param legend_location: the position of legend + :param y_max_lim: the maximum on the y axis (if None, put the + maximum of the bands) + :param y_min_lim: the minimum on the y axis (if None, put the + minimum of the bands) + :param y_origin: the new origin of the y axis -> all bands are replaced + by bands-y_origin + :param prettify_format: if None, use the default prettify format. Otherwise + specify a string with the prettifier to use. + :param kwargs: additional customization variables; only a subset is + accepted, see internal variable 'valid_additional_keywords + """ + # pylint: disable=too-many-arguments,too-many-locals + + # Only these keywords are accepted in kwargs, and then set into the json + valid_additional_keywords = [ + 'bands_color', # Color of band lines + 'bands_linewidth', # linewidth of bands + 'bands_linestyle', # linestyle of bands + 'bands_marker', # marker for bands + 'bands_markersize', # size of the marker of bands + 'bands_markeredgecolor', # marker edge color for bands + 'bands_markeredgewidth', # marker edge width for bands + 'bands_markerfacecolor', # marker face color for bands + 'use_latex', # If true, use latex to render captions + ] + + # Note: I do not want to import matplotlib here, for two reasons: + # 1. I would like to be able to print the script for the user + # 2. I don't want to mess up with the user matplotlib backend + # (that I should do if the user does not have a X server, but that + # I do not want to do if he's e.g. in jupyter) + # Therefore I just create a string that can be executed as needed, e.g. with eval. + # I take care of sanitizing the output. + # if prettify_format is None: + # # Default. Specified like this to allow caller functions to pass 'None' + # prettify_format = 'latex_seekpath' + + # # The default for use_latex is False + # join_symbol = r'\textbar{}' if kwargs.get('use_latex', False) else '|' + + plot_info = self._get_stabilityplot_data() + + all_data = {} + + all_data['x'] = plot_info['x'].tolist() + all_data['compound_lines'] = plot_info['y'].tolist() + all_data['stability_vertices'] = plot_info['vertices'].tolist() + all_data['boundary_lines'] = list(plot_info['boundary_lines']) + all_data['all_compounds'] = self.index + if self.property_map: + all_data['grid'] = plot_info['grid'] + all_data['property'] = plot_info['property'] + # all_data['grid_dx'] = (np.amax(plot_info['vertices'][:, 0]) - np.amin(plot_info['vertices'][:, 0]))/50 + # all_data['grid_dy'] = (np.amax(plot_info['vertices'][:, 1]) - np.amin(plot_info['vertices'][:, 1]))/50 + all_data['legend_location'] = legend_location + all_data['xaxis_label'] = f'Chemical potential of {self.column[0]} (eV)' + all_data['yaxis_label'] = f'Chemical potential of {self.column[1]} (eV)' + all_data['title'] = title + # if comments: + # all_data['comment'] = prepare_header_comment(self.uuid, plot_info, comment_char='#') + + # axis limits + width = np.amax(plot_info['vertices'][:,0]) - np.amin(plot_info['vertices'][:,0]) # width of the stability region + height = np.amax(plot_info['vertices'][:,1]) - np.amin(plot_info['vertices'][:,1]) # height of the stability region + if y_max_lim is None: + y_max_lim = min(0, np.amax(plot_info['vertices'][:,1])+0.2*height) + if y_min_lim is None: + y_min_lim = np.amin(plot_info['vertices'][:,1])-0.2*height + if x_max_lim is None: + x_max_lim = min(0, np.amax(plot_info['vertices'][:,0])+0.2*width) + if x_min_lim is None: + x_min_lim = np.amin(plot_info['vertices'][:,0])-0.2*width + all_data['x_min_lim'] = x_min_lim + all_data['x_max_lim'] = x_max_lim + all_data['y_min_lim'] = y_min_lim + all_data['y_max_lim'] = y_max_lim + + for key, value in kwargs.items(): + if key not in valid_additional_keywords: + raise TypeError(f"_matplotlib_get_dict() got an unexpected keyword argument '{key}'") + all_data[key] = value + + return all_data + + def _prepare_mpl_singlefile(self, *args, **kwargs): + """ + Prepare a python script using matplotlib to plot the bands + + For the possible parameters, see documentation of + :py:meth:`~aiida.orm.nodes.data.array.bands.BandsData._matplotlib_get_dict` + """ + from aiida.common import json + + all_data = self._matplotlib_get_dict(*args, **kwargs) + + s_header = MATPLOTLIB_HEADER_TEMPLATE.substitute() + s_import = MATPLOTLIB_IMPORT_DATA_INLINE_TEMPLATE.substitute(all_data_json=json.dumps(all_data, indent=2)) + s_body = self._get_mpl_body_template(all_data) + # s_body = MATPLOTLIB_BODY_TEMPLATE.substitute() + s_footer = MATPLOTLIB_FOOTER_TEMPLATE_SHOW.substitute() + + string = s_header + s_import + s_body + s_footer + + return string.encode('utf-8'), {} + + def _prepare_mpl_pdf(self, main_file_name='', *args, **kwargs): # pylint: disable=keyword-arg-before-vararg,unused-argument + """ + Prepare a python script using matplotlib to plot the stability region, with the JSON + returned as an independent file. + """ + import os + import tempfile + import subprocess + import sys + + from aiida.common import json + + all_data = self._matplotlib_get_dict(*args, **kwargs) + + # Use the Agg backend + s_header = MATPLOTLIB_HEADER_AGG_TEMPLATE.substitute() + s_import = MATPLOTLIB_IMPORT_DATA_INLINE_TEMPLATE.substitute(all_data_json=json.dumps(all_data, indent=2)) + s_body = self._get_mpl_body_template(all_data) + + # I get a temporary file name + handle, filename = tempfile.mkstemp() + os.close(handle) + os.remove(filename) + + escaped_fname = filename.replace('"', '\"') + + s_footer = MATPLOTLIB_FOOTER_TEMPLATE_EXPORTFILE.substitute(fname=escaped_fname, format='pdf') + + string = s_header + s_import + s_body + s_footer + + # I don't exec it because I might mess up with the matplotlib backend etc. + # I run instead in a different process, with the same executable + # (so it should work properly with virtualenvs) + # with tempfile.NamedTemporaryFile(mode='w+') as handle: + # handle.write(string) + # handle.flush() + # subprocess.check_output([sys.executable, handle.name]) + + if not os.path.exists(filename): + raise RuntimeError('Unable to generate the PDF...') + + with open(filename, 'rb', encoding=None) as handle: + imgdata = handle.read() + os.remove(filename) + + return imgdata, {} + + def _prepare_mpl_withjson(self, main_file_name='', *args, **kwargs): # pylint: disable=keyword-arg-before-vararg + """ + Prepare a python script using matplotlib to plot the bands, with the JSON + returned as an independent file. + + For the possible parameters, see documentation of + :py:meth:`~aiida.orm.nodes.data.array.bands.BandsData._matplotlib_get_dict` + """ + import os + + from aiida.common import json + + all_data = self._matplotlib_get_dict(*args, main_file_name=main_file_name, **kwargs) + + json_fname = os.path.splitext(main_file_name)[0] + '_data.json' + # Escape double_quotes + json_fname = json_fname.replace('"', '\"') + + ext_files = {json_fname: json.dumps(all_data, indent=2).encode('utf-8')} + + s_header = MATPLOTLIB_HEADER_TEMPLATE.substitute() + s_import = MATPLOTLIB_IMPORT_DATA_FROMFILE_TEMPLATE.substitute(json_fname=json_fname) + s_body = self._get_mpl_body_template(all_data) + s_footer = MATPLOTLIB_FOOTER_TEMPLATE_SHOW.substitute() + + string = s_header + s_import + s_body + s_footer + + return string.encode('utf-8'), ext_files + + @staticmethod + def _get_mpl_body_template(all_data): + if all_data.get('grid'): + s_body = MATPLOTLIB_BODY_TEMPLATE.substitute(plot_code=WITH_PROPERTY) + else: + s_body = MATPLOTLIB_BODY_TEMPLATE.substitute(plot_code=WITHOUT_PROPERTY) + return s_body + + def show_mpl(self, **kwargs): + """ + Call a show() command for the band structure using matplotlib. + This uses internally the 'mpl_singlefile' format, with empty + main_file_name. + + Other kwargs are passed to self._exportcontent. + """ + exec(*self._exportcontent(fileformat='mpl_singlefile', main_file_name='', **kwargs)) + + def _prepare_json(self, main_file_name='', comments=True): # pylint: disable=unused-argument + """ + Prepare a json file in a format compatible with the AiiDA band visualizer + + :param comments: if True, print comments (if it makes sense for the given + format) + """ + from aiida import get_file_header + from aiida.common import json + + json_dict = self._get_band_segments(cartesian=True) + json_dict['original_uuid'] = self.uuid + + if comments: + json_dict['comments'] = get_file_header(comment_char='') + + return json.dumps(json_dict).encode('utf-8'), {} + +MATPLOTLIB_HEADER_AGG_TEMPLATE = Template( + """# -*- coding: utf-8 -*- + +import matplotlib +matplotlib.use('Agg') + +from matplotlib import rc +# Uncomment to change default font +#rc('font',**{'family':'sans-serif','sans-serif':['Helvetica']}) +rc('font', **{'family': 'serif', 'serif': ['Computer Modern', 'CMU Serif', 'Times New Roman', 'DejaVu Serif']}) +# To use proper font for, e.g., Gamma if usetex is set to False +rc('mathtext', fontset='cm') + +rc('text', usetex=True) + +import pylab as pl + +# I use json to make sure the input is sanitized +import json + +print_comment = False +""" +) + +MATPLOTLIB_HEADER_TEMPLATE = Template( + """# -*- coding: utf-8 -*- + +from matplotlib import rc +# Uncomment to change default font +#rc('font',**{'family':'sans-serif','sans-serif':['Helvetica']}) +rc('font', **{'family': 'serif', 'serif': ['Computer Modern', 'CMU Serif', 'Times New Roman', 'DejaVu Serif']}) +# To use proper font for, e.g., Gamma if usetex is set to False +rc('mathtext', fontset='cm') + +rc('text', usetex=True) + +import matplotlib.pyplot as plt +import numpy as np + +# I use json to make sure the input is sanitized +import json + +print_comment = False + +def prettify_compound_name(name): + pretty_name = '' + for char in name: + if char.isnumeric(): + pretty_name += '$$_'+char+'$$' + else: + pretty_name += char + return pretty_name + +""" +) + +MATPLOTLIB_IMPORT_DATA_INLINE_TEMPLATE = Template('''all_data_str = r"""$all_data_json""" +''') + +MATPLOTLIB_IMPORT_DATA_FROMFILE_TEMPLATE = Template( + """with open("$json_fname", encoding='utf8') as f: + all_data_str = f.read() +""" +) + +WITHOUT_PROPERTY = ''' +ax.fill(vertices[:,0], vertices[:,1], color='gray', alpha=0.5) +''' + +WITH_PROPERTY = ''' +from scipy.interpolate import griddata +import matplotlib.colors as colors +import matplotlib.cm as cm + +x_min = np.amin(vertices[:,0]) +x_max = np.amax(vertices[:,0]) +y_min = np.amin(vertices[:,1]) +y_max = np.amax(vertices[:,1]) +num_point = 100 +X = np.linspace(x_min, x_max, num_point) +Y = np.linspace(y_min, y_max, num_point) +xx, yy = np.meshgrid(X, Y) + +interp_c = griddata(all_data['grid'], all_data['property'], (xx, yy), method='linear') +im = ax.pcolor(X, Y, interp_c, norm=colors.LogNorm(vmin=np.nanmin(interp_c)*0.95, vmax=np.nanmax(interp_c)*0.95*1.05), cmap=cm.RdBu, shading='auto') +cbar = fig.colorbar(im, ax=ax, extend='max') +cbar.ax.set_ylabel('Concentration (/cm$^3$)', fontsize=14, rotation=-90, va="bottom") +''' + + +MATPLOTLIB_BODY_TEMPLATE = Template( + """all_data = json.loads(all_data_str) + +if not all_data.get('use_latex', False): + rc('text', usetex=False) + +# Option for bands (all, or those of type 1 if there are two spins) +further_plot_options = {} +further_plot_options['color'] = all_data.get('bands_color', 'k') +further_plot_options['linewidth'] = all_data.get('bands_linewidth', 0.5) +further_plot_options['linestyle'] = all_data.get('bands_linestyle', None) +further_plot_options['marker'] = all_data.get('bands_marker', None) +further_plot_options['markersize'] = all_data.get('bands_markersize', None) +further_plot_options['markeredgecolor'] = all_data.get('bands_markeredgecolor', None) +further_plot_options['markeredgewidth'] = all_data.get('bands_markeredgewidth', None) +further_plot_options['markerfacecolor'] = all_data.get('bands_markerfacecolor', None) + +fig, ax = plt.subplots() + +for h, v in zip(all_data['x'], all_data['compound_lines']): + ax.plot(h, v, linestyle='dashed', linewidth=1.0, color='k') + +for cmp in all_data['boundary_lines']: + idx = all_data['all_compounds'].index(cmp) + ax.plot(all_data['x'][idx], all_data['compound_lines'][idx], linewidth=1.5, label=prettify_compound_name(cmp)) + +vertices = np.array(all_data['stability_vertices']) +ax.scatter(vertices[:,0], vertices[:,1], color='k') + +${plot_code} + +ax.set_xlim([all_data['x_min_lim'], all_data['x_max_lim']]) +ax.set_ylim([all_data['y_min_lim'], all_data['y_max_lim']]) +# p.xaxis.grid(True, which='major', color='#888888', linestyle='-', linewidth=0.5) + +if all_data['title']: + ax.set_title(all_data['title']) +if all_data['legend_location']: + ax.legend(loc=all_data['legend_location']) +ax.set_xlabel(all_data['xaxis_label']) +ax.set_ylabel(all_data['yaxis_label']) + +try: + if print_comment: + print(all_data['comment']) +except KeyError: + pass +""" +) + +MATPLOTLIB_FOOTER_TEMPLATE_SHOW = Template("""plt.show()""") + +MATPLOTLIB_FOOTER_TEMPLATE_EXPORTFILE = Template("""plt.savefig("$fname", format="$format")""") + +MATPLOTLIB_FOOTER_TEMPLATE_EXPORTFILE_WITH_DPI = Template("""plt.savefig("$fname", format="$format", dpi=$dpi)""") \ No newline at end of file diff --git a/aiida_defects/formation_energy/chemical_potential/__init__.py b/aiida_defects/formation_energy/chemical_potential/__init__.py new file mode 100644 index 0000000..4d27567 --- /dev/null +++ b/aiida_defects/formation_energy/chemical_potential/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## diff --git a/aiida_defects/formation_energy/chemical_potential/chemical_potential.py b/aiida_defects/formation_energy/chemical_potential/chemical_potential.py new file mode 100644 index 0000000..2bedb51 --- /dev/null +++ b/aiida_defects/formation_energy/chemical_potential/chemical_potential.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +from aiida.engine import WorkChain, calcfunction, ToContext, while_ +from aiida.orm import Float, Int, Str, List, Bool, Dict, ArrayData +from aiida_defects.data.data import StabilityData +import sys +import numpy as np +from pymatgen.core.composition import Composition +from pymatgen.core.periodic_table import Element +from itertools import combinations + +from .utils import * + +class ChemicalPotentialWorkchain(WorkChain): + """ + Compute the range of chemical potential of different elements which are consistent with the stability + of that compound. + Here we implement method similar to Buckeridge et al., (https://doi.org/10.1016/j.cpc.2013.08.026), + """ + + @classmethod + def define(cls, spec): + super(ChemicalPotentialWorkchain, cls).define(spec) + spec.input("formation_energy_dict", valid_type=Dict, + help="The formation energies of all compounds in the phase diagram to which belong the material of interest") + spec.input("compound", valid_type=Str, + help="The name of the material of interest") + spec.input("dependent_element", valid_type=Str, + help="In a N-element phase diagram, the chemical potential of depedent_element is fixed by that of the other N-1 elements") + spec.input("dopant_elements", valid_type=List, default=lambda: List(), + help="The aliovalent dopants that might be introduce into the prestine material. Several dopants might be present in co-doping scenario.") + spec.input("ref_energy", valid_type=Dict, + help="The reference chemical potential of elements in the structure") + spec.input("tolerance", valid_type=Float, default=lambda: Float(1E-4), + help="Use to determine if a point in the chemical potential space is a corner of the stability region or not") + spec.input("grid_points", valid_type=Int, default=lambda: Int(25), + help="The number of point on each axis to generate the grid of the stability region. This grid is needed to determine the centroid or to plot concentration or defect formation energy directly on top of the stability region") + + spec.outline( + cls.setup, + cls.generate_matrix_of_constraints, + cls.solve_matrix_of_constraints, + cls.get_chemical_potential, + cls.get_stability_region_data + ) + spec.output('stability_vertices', valid_type=Dict) + spec.output('matrix_of_constraints', valid_type=Dict) + spec.output('chemical_potential', valid_type=Dict) + #spec.output('stability_region', valid_type=StabilityData) + + spec.exit_code(601, "ERROR_CHEMICAL_POTENTIAL_FAILED", + message="The stability region can't be determined. The compound is probably unstable" + ) + spec.exit_code(602, "ERROR_INVALID_DEPENDENT_ELEMENT", + message="The given dependent element is invalid." + ) + spec.exit_code(603, "ERROR_INVALID_DOPANT_ELEMENT", + message="The given dopant element(s) is invalid." + ) + spec.exit_code(604, "ERROR_INVALID_NUMBER_OF_ELEMENTS", + message="The number of elements is invalid to generate stability data." + ) + + def setup(self): + if self.inputs.dependent_element.value in self.inputs.dopant_elements.get_list(): + self.report('In the case of aliovalent substitution, the dopant element has to be different from dependent element. Please choose a different dependent element or dopant(s).') + return self.exit_codes.ERROR_INVALID_DEPENDENT_ELEMENT + + for atom in self.inputs.dopant_elements.get_list(): + if atom in Composition(self.inputs.compound.value): + self.report('The dopant element has to be different from the constitutive elements of the given compound, {}.'.format(self.inputs.compound.value)) + return self.exit_codes.ERROR_INVALID_DOPANT_ELEMENT + + composition = Composition(self.inputs.compound.value) + element_list = [atom.symbol for atom in composition] + + if self.inputs.dopant_elements.get_list(): # check if the list empty + element_list += [atom for atom in self.inputs.dopant_elements.get_list()] # List concatenation + N_species = len(composition) + len(self.inputs.dopant_elements.get_list()) + else: + N_species = len(composition) + + if self.inputs.dependent_element.value not in element_list: + self.report('The dependent element must be one of the constitutive elements of the given compound, {}. Please choose a different dependent element.'.format(self.inputs.compound.value)) + return self.exit_codes.ERROR_INVALID_DEPENDENT_ELEMENT + + self.ctx.N_species = Int(N_species) + formation_energy_dict = self.inputs.formation_energy_dict.get_dict() + + # check if the compound is stable or not. If not shift its energy down to put it on the convex hull and issue a warning. + E_hull = get_e_above_hull(self.inputs.compound.value, element_list, formation_energy_dict) + if E_hull > 0: + self.report('WARNING! The compound {} is predicted to be unstable. For the purpose of determining the stability region, we shift its formation energy down so that it is on the convex hull. Use with care!'.format(self.inputs.compound.value)) + formation_energy_dict[self.inputs.compound.value] -= composition.num_atoms*(E_hull+0.005) # the factor 0.005 is added for numerical reason + + self.ctx.formation_energy_dict = Dict(formation_energy_dict) + + def generate_matrix_of_constraints(self): + + ############################################################################## + # Construct matrix containing all linear equations. The last column is the rhs + # of the system of equations + ############################################################################## + + all_constraints_coefficients = get_full_matrix_of_constraints( + self.ctx.formation_energy_dict, + self.inputs.compound, + self.inputs.dependent_element, + self.inputs.dopant_elements, + ) + # print(Dict_to_pandas_df(all_constraints_coefficients)) + # self.ctx.master_eqn = get_master_equation(all_constraints_coefficients, self.inputs.compound) + self.ctx.master_eqn = get_master_equation( + self.ctx.formation_energy_dict, + self.inputs.compound, + self.inputs.dependent_element, + self.inputs.dopant_elements + ) + # print(Dict_to_pandas_df(self.ctx.master_eqn)) + self.ctx.matrix_eqns = get_reduced_matrix_of_constraints( + all_constraints_coefficients, + self.inputs.compound, + self.inputs.dependent_element, + ) + # print(Dict_to_pandas_df(self.ctx.matrix_eqns)) + self.out('matrix_of_constraints', self.ctx.matrix_eqns) + + def solve_matrix_of_constraints(self): + self.ctx.stability_vertices = get_stability_vertices( + self.ctx.master_eqn, + self.ctx.matrix_eqns, + self.inputs.compound, + self.inputs.dependent_element, + self.inputs.tolerance + ) + #self.report('The stability vertices are : {}'.format(np.around(self.ctx.stability_vertices.get_dict()['data'], 4))) + # print(Dict_to_pandas_df(self.ctx.stability_vertices)) + self.out("stability_vertices", self.ctx.stability_vertices) + + def get_chemical_potential(self): + centroid = get_centroid_of_stability_region( + self.ctx.stability_vertices, + self.ctx.master_eqn, + self.ctx.matrix_eqns, + self.inputs.compound, + self.inputs.dependent_element, + self.inputs.grid_points, + self.inputs.tolerance + ) + self.ctx.centroid = centroid + self.report('Centroid of the stability region is {}'.format(dict(zip(centroid.get_dict()['column'], centroid.get_dict()['data'][0])))) + + self.ctx.chemical_potential = get_absolute_chemical_potential( + centroid, + self.inputs.ref_energy, + ) + self.out('chemical_potential', self.ctx.chemical_potential) + self.report('The chemical potential is {}'.format(self.ctx.chemical_potential.get_dict())) + + def get_stability_region_data(self): + + vertices = self.ctx.stability_vertices.get_dict() + if np.array(vertices['data']).shape[1] == 1: + self.report('The compound has to contain more than one element') + return self.exit_codes.ERROR_INVALID_NUMBER_OF_ELEMENTS + elif np.array(vertices['data']).shape[1] == 2: + self.report('The stability region is simply a line segment and is not plotted') + return self.exit_codes.ERROR_INVALID_NUMBER_OF_ELEMENTS + elif np.array(vertices['data']).shape[1] == 3: + sub_matrix_eqns = self.ctx.matrix_eqns + sub_vertices = self.ctx.stability_vertices + else: + centroid = self.ctx.centroid.get_dict() + fixed_chempot = Dict({k: np.array(centroid['data'])[:,i] for i, k in enumerate(centroid['column'][:-3])}) # Keep the last 3 columns + sub_master_eqn = substitute_chemical_potential(self.ctx.master_eqn, fixed_chempot) + sub_matrix_eqns = substitute_chemical_potential(self.ctx.matrix_eqns, fixed_chempot) + # print(Dict_to_pandas_df(sub_matrix_eqns)) + sub_vertices = get_stability_vertices( + sub_master_eqn, + sub_matrix_eqns, + self.inputs.compound, + self.inputs.dependent_element, + self.inputs.tolerance + ) + + #stability_region = get_StabilityData( + # sub_matrix_eqns, + # sub_vertices, + # self.inputs.compound, + # self.inputs.dependent_element, + # ) + #self.out('stability_region', stability_region) diff --git a/aiida_defects/formation_energy/chemical_potential/utils.py b/aiida_defects/formation_energy/chemical_potential/utils.py new file mode 100644 index 0000000..ef25767 --- /dev/null +++ b/aiida_defects/formation_energy/chemical_potential/utils.py @@ -0,0 +1,412 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +from aiida.engine import calcfunction +import numpy as np +import pandas as pd +from pymatgen.core.composition import Composition +from aiida.orm import ArrayData, Float, Dict, List +from pymatgen.core.periodic_table import Element +from itertools import combinations +from pymatgen.analysis.phase_diagram import * +from pymatgen.entries.computed_entries import ComputedEntry + +def pandas_df_to_Dict(df, index=False): + ''' + Helper function to convert a pandas dataframe to AiiDA Dict. + If index=False, the index of df won't be converted to (keys, values) pair in the Dict + ''' + if index: + return Dict({'column': df.columns, 'index': df.index, 'data': df.to_numpy()}) + else: + return Dict({'column': df.columns, 'data': df.to_numpy()}) + +def Dict_to_pandas_df(py_dict): + ''' + Helper function to convert a dict to a pandas dataframe + ''' + if 'index' in py_dict.keys(): + return pd.DataFrame(np.array(py_dict['data']), index=py_dict['index'], columns=py_dict['column']) + else: + return pd.DataFrame(np.array(py_dict['data']), columns=py_dict['column']) + +def get_full_matrix_of_constraints(formation_energy_dict, compound, dependent_element, dopant): + ''' + The systems of linear constraints (before eliminating the dependent variable and the 'compound'), i.e. matrix of constraints is constructed as + a pandas dataframe. Each columns corresponds to each element in the compounds and dopants ('Li', 'P', ...) while the last + column is the formation energy (per fu) of each stable compounds in the phase diagram. The column before the last column is + always reserved for the depedent element. Each row is indexed by the formula of each stable compound. + When it is not possible to use pandas dataframe for ex. to pass as argument to a calcfuntion, the dataframe is 'unpacked' as + a python dictionary in the form {'column': , 'index': , 'data': } + ''' + + formation_energy_dict = formation_energy_dict.get_dict() + compound = compound.value + dependent_element = dependent_element.value + dopant = dopant.get_list() + + compound_of_interest = Composition(compound) + + # Setting up the matrix of constraints as pd dataframe and initialize it to zeros. + stable_compounds, element_order = [], [] + for key in formation_energy_dict.keys(): + stable_compounds.append(key) + for key in compound_of_interest: + stable_compounds.append(key.symbol) + + element_order = [atom.symbol for atom in compound_of_interest if atom.symbol != dependent_element] + if dopant == []: + element_order.extend([dependent_element, 'Ef']) + N_species = len(compound_of_interest) + else: + element_order.extend(dopant+[dependent_element, 'Ef']) + N_species = len(compound_of_interest) + len(dopant) + stable_compounds.extend(dopant) + + eqns = pd.DataFrame(np.zeros((len(stable_compounds), len(element_order))), index=stable_compounds, columns=element_order) + + # Setting the coefficients of the matrix of constraint + # First, loop through all the competing phases + for k, v in formation_energy_dict.items(): + composition = Composition(k) + for element in composition: + eqns.loc[k, element.symbol] = composition[element] + eqns.loc[k, 'Ef'] = v + # Then, loop over all elemental phases + for element in compound_of_interest: + eqns.loc[element.symbol, element.symbol] = 1.0 + if dopant: + for element in dopant: + eqns.loc[element, element] = 1.0 + + return pandas_df_to_Dict(eqns, index=True) + +# @calcfunction +# def get_master_equation(raw_constraint_coefficients, compound): +# ''' +# The 'master' equation is simply the equality corresponding to the formation energy of the +# compound under consideration. For ex. if we are studying the defect in Li3PO4, the master +# equation is simply: 3*mu_Li + mu_P + 4*mu_O = Ef where Ef is the formation energy per fu +# of Li3PO4. This equation is needed to replace the dependent chemical potential from the set +# of other linear constraints and to recover the chemical potential of the dependent element +# from the chemical potentials of independent elements +# ''' +# all_coefficients = raw_constraint_coefficients.get_dict() +# eqns = Dict_to_pandas_df(all_coefficients) +# master_eqn = eqns.loc[[compound.value],:] + +# return pandas_df_to_Dict(master_eqn, index=True) + +@calcfunction +def get_master_equation(formation_energy_dict, compound, dependent_element, dopant): + ''' + The 'master' equation is simply the equality corresponding to the formation energy of the + compound under consideration. For ex. if we are studying the defect in Li3PO4, the master + equation is simply: 3*mu_Li + mu_P + 4*mu_O = Ef where Ef is the formation energy per fu + of Li3PO4. This equation is needed to replace the dependent chemical potential from the set + of other linear constraints and to recover the chemical potential of the dependent element + from the chemical potentials of independent elements + ''' + Ef_dict = formation_energy_dict.get_dict() + compound = compound.value + composition = Composition(compound) + dependent_element = dependent_element.value + dopant = dopant.get_list() + + element_order = [atom.symbol for atom in composition if atom.symbol != dependent_element] + if dopant == []: + element_order.extend([dependent_element, 'Ef']) + else: + element_order.extend(dopant+[dependent_element, 'Ef']) + master_eqn = pd.DataFrame(np.zeros((1, len(element_order))), index=[compound], columns=element_order) + + for atom in composition: + master_eqn.loc[compound, atom.symbol] = composition[atom] + master_eqn.loc[compound, 'Ef'] = Ef_dict[compound] + + return pandas_df_to_Dict(master_eqn, index=True) + + +@calcfunction +def get_reduced_matrix_of_constraints(full_matrix_of_constraints, compound, dependent_element): + ''' + The reduced matrix of constraints is obtained from the full matrix of constraint by eliminating + the row corresponding to the master equation and the column associated with the dependent element + after substituting the chemical potential of the dependent element by that of the independent + elements using the master equation (which at this stage is the first row of the full matrix of + constraints). Therefore, if the shape of the full matrix of constraint is NxM, then the shape + of the reduced matrix of constraints is (N-1)x(M-1) + ''' + compound = compound.value + dependent_element = dependent_element.value + all_coefficients = full_matrix_of_constraints.get_dict() + eqns = Dict_to_pandas_df(all_coefficients) + master_eqn = eqns.loc[[compound],:] + M = master_eqn.loc[compound].to_numpy() + M = np.reshape(M, (1,-1)) + + # Substitute the dependent element (variable) from the equations + tmp = np.reshape(eqns[dependent_element].to_numpy(), (-1,1))*M/master_eqn.loc[compound, dependent_element] + eqns = pd.DataFrame(eqns.to_numpy()-tmp, index=eqns.index, columns=eqns.columns) + # Remove master equation and the column corresponding to the dependent element from the full matrix of constraints + eqns = eqns.drop(compound) + eqns = eqns.drop(columns=dependent_element) + # print(eqns) + + return pandas_df_to_Dict(eqns, index=True) + +@calcfunction +def get_stability_vertices(master_eqn, matrix_eqns, compound, dependent_element, tolerance): + ''' + Solving the (reduced) matrix of constraints to obtain the vertices of the stability region. + The last column (or coordinate) corresponds to the dependent element. + ''' + master_eqn = master_eqn.get_dict() + matrix_eqns = matrix_eqns.get_dict() + set_of_constraints = np.array(matrix_eqns['data']) + compound = compound.value + dependent_element = dependent_element.value + tolerance = tolerance.value + N_species = set_of_constraints.shape[1] + + ### Look at all combination of lines (or plans or hyperplans) and find their intersections + comb = combinations(np.arange(np.shape(set_of_constraints)[0]), N_species-1) + intersecting_points = [] + for item in list(comb): + try: + point = np.linalg.solve(set_of_constraints[item,:-1], set_of_constraints[item,-1]) + intersecting_points.append(point) + except np.linalg.LinAlgError: + ### Singular matrix: lines or (hyper)planes are parallels therefore don't have any intersection + pass + + ### Determine the points that form the vertices of stability region. These are intersecting point that verify all the constraints. + intersecting_points = np.array(intersecting_points) + get_constraint = np.dot(intersecting_points, set_of_constraints[:,:-1].T) + check_constraint = (get_constraint - np.reshape(set_of_constraints[:,-1] ,(1, set_of_constraints.shape[0]))) <= tolerance + bool_mask = [not(False in x) for x in check_constraint] + corners_of_stability_region = intersecting_points[bool_mask] + ### In some cases, we may have several solutions corresponding to the same points. Hence, the remove_duplicate method + corners_of_stability_region = remove_duplicate(corners_of_stability_region) + + # if corners_of_stability_region.size != 0: + # self.report('The stability region cannot be determined. The compound {} is probably unstable'.format(compound)) + # return self.exit_codes.ERROR_CHEMICAL_POTENTIAL_FAILED + + stability_corners = pd.DataFrame(corners_of_stability_region, columns=matrix_eqns['column'][:-1]) + master_eqn = Dict_to_pandas_df(master_eqn) + # get the chemical potentials of the dependent element + dependent_chempot = get_dependent_chempot(master_eqn, stability_corners.to_dict(orient='list'), compound, dependent_element) + stability_corners = np.append(stability_corners, np.reshape(dependent_chempot, (-1,1)), axis =1) + stability_vertices = Dict({'column': matrix_eqns['column'][:-1]+[dependent_element], 'data': stability_corners}) + + return stability_vertices + +def get_dependent_chempot(master_eqn, chempots, compound, dependent_element): + ''' + Calculate the chemical potential of the 'dependent' elements from the chemical potentials of 'independent' elements + ''' + tmp = 0 + for col in master_eqn.columns: + if col != dependent_element and col != 'Ef': + tmp += np.array(chempots[col])*master_eqn.loc[compound, col] + return (master_eqn.loc[compound, 'Ef']-tmp)/master_eqn.loc[compound, dependent_element] + + +@calcfunction +def get_centroid_of_stability_region(stability_corners, master_eqn, matrix_eqns, compound, dependent_element, grid_points, tolerance): + ''' + Use to determine centroid or in some cases the center of the stability region. The center is defined as the average + coordinates of the vertices while a centroid is the average cooridinates of every point inside the polygone or polyhedron, + i.e. its center of mass. + For binary compounds, the stability region is a one-dimensional segment. The centroid coincides with the center. + For ternary, quarternary and quinary compounds, the centroid is returned. + For compound with more that 5 elements, the center is returned. + ''' + stability_corners = np.array(stability_corners.get_dict()['data']) + master_eqn = master_eqn.get_dict() + matrix_eqns = matrix_eqns.get_dict() + compound = compound.value + dependent_element = dependent_element.value + tolerance = tolerance.value + grid_points = grid_points.value + + points_in_stability_region = get_points_in_stability_region(stability_corners[:,:-1], np.array(matrix_eqns['data']), grid_points, tolerance) + ctr_stability = get_centroid(points_in_stability_region) #without the dependent element + ctr_stability = pd.DataFrame(np.reshape(ctr_stability, (1, -1)), columns=matrix_eqns['column'][:-1]) + + master_eqn = Dict_to_pandas_df(master_eqn) + # Add the corresponding chemical potential of the dependent element + dependent_chempot = get_dependent_chempot(master_eqn, ctr_stability.to_dict(orient='list'), compound, dependent_element) + ctr_stability = np.append(ctr_stability, np.reshape(dependent_chempot, (-1,1)), axis=1) + ctr_stability = Dict({'column': matrix_eqns['column'][:-1]+[dependent_element], 'data': ctr_stability}) + + return ctr_stability + +def get_e_above_hull(compound, element_list, formation_energy_dict): + ''' + Get the energy above the convex hull. When the compound is unstable, e_hull > 0. + ''' + composition = Composition(compound) + mp_entries = [] + + idx = 0 + for i, (material, Ef) in enumerate(formation_energy_dict.items()): + if material == compound: + idx = i + mp_entries.append(ComputedEntry(Composition(material), Ef)) + for ref in element_list: + mp_entries.append(ComputedEntry(Composition(ref), 0.0)) + #mp_entries.append(ComputedEntry(composition, E_formation)) + + pd = PhaseDiagram(mp_entries) + ehull = pd.get_e_above_hull(mp_entries[idx]) + + return ehull + +def same_composition(compound_1, compound_2): + composition_1 = Composition(compound_1) + composition_2 = Composition(compound_2) + list_1 = [ele.symbol for ele in composition_1] + list_2 = [ele.symbol for ele in composition_2] + list_1.sort() + list_2.sort() + if list_1 != list_2: + return False + else: + number_ele_1 = [composition_1[ele] for ele in list_1] + number_ele_2 = [composition_2[ele] for ele in list_2] + return number_ele_1 == number_ele_2 + +def is_point_in_array(ref_point, ref_array): + for point in ref_array: + if np.array_equal(ref_point, point): + return True + return False + +def remove_duplicate(array): + non_duplicate_array = [] + for point in array: + if not is_point_in_array(point, non_duplicate_array): + non_duplicate_array.append(point) + return np.array(non_duplicate_array) + +def get_points_in_stability_region(stability_corners, matrix_eqns, N_point, tolerance): + dim = stability_corners.shape[1] + if dim ==1: + return stability_corners + elif dim == 2: + [xmin, ymin] = np.amin(stability_corners, axis=0) + [xmax, ymax] = np.amax(stability_corners, axis=0) + x = np.linspace(xmin, xmax, N_point) + y = np.linspace(ymin, ymax, N_point) + xx, yy = np.meshgrid(x, y) + points = np.append(xx.reshape(-1,1),yy.reshape(-1,1),axis=1) + elif dim == 3: + [xmin, ymin, zmin] = np.amin(stability_corners, axis=0) + [xmax, ymax, zmax] = np.amax(stability_corners, axis=0) + x = np.linspace(xmin, xmax, N_point) + y = np.linspace(ymin, ymax, N_point) + z = np.linspace(zmin, zmax, N_point) + xx, yy, zz = np.meshgrid(x, y, z) + points = np.append(xx.reshape(-1,1), yy.reshape(-1,1), axis=1) + points = np.append(points, zz.reshape(-1,1), axis=1) + elif dim == 4: + [xmin, ymin, zmin, umin] = np.amin(stability_corners, axis=0) + [xmax, ymax, zmax, umax] = np.amax(stability_corners, axis=0) + x = np.linspace(xmin, xmax, N_point) + y = np.linspace(ymin, ymax, N_point) + z = np.linspace(zmin, zmax, N_point) + u = np.linspace(umin, umax, N_point) + xx, yy, zz, uu = np.meshgrid(x, y, z, u) + points = np.append(xx.reshape(-1,1), yy.reshape(-1,1), axis=1) + points = np.append(points, zz.reshape(-1,1), axis=1) + points = np.append(points, uu.reshape(-1,1), axis=1) + else: + print('Not yet implemented for systems having more than 5 elements. Use center instead of centroid') + return stability_corners + + get_constraint = np.dot(points, matrix_eqns[:,:-1].T) + check_constraint = (get_constraint - np.reshape(matrix_eqns[:,-1], (1, matrix_eqns.shape[0]))) <= tolerance + bool_mask = [not(False in x) for x in check_constraint] + points_in_stable_region = points[bool_mask] + + return points_in_stable_region + +def get_centroid(stability_region): + return np.mean(stability_region, axis=0) + +@calcfunction +def substitute_chemical_potential(matrix_eqns, fixed_chempot): + ''' + substitute chemical potentials in the matrix of constraints by some fixed values. + Useful for ex. for determining the 'slice' of stability region. + ''' + matrix_eqns = Dict_to_pandas_df(matrix_eqns.get_dict()) + fixed_chempot = fixed_chempot.get_dict() + for spc in fixed_chempot.keys(): + matrix_eqns.loc[:, 'Ef'] -= matrix_eqns.loc[:, spc]*fixed_chempot[spc] + matrix_eqns = matrix_eqns.drop(columns=spc) + + for spc in fixed_chempot.keys(): + if spc in matrix_eqns.index: + # print('Found!') + matrix_eqns = matrix_eqns.drop(spc) + # print(matrix_eqns) + return pandas_df_to_Dict(matrix_eqns, index=True) + +def Order_point_clockwise(points): + ''' + The vertices of the stability region has to be ordered clockwise or counter-clockwise for plotting. + Work only in 2D stability region + + points: 2d numpy array + + ''' + if points.shape[1] == 3: + # Excluding the column corresponding to the dependent element (last column) + points = points[:,:-1] + center = np.mean(points, axis=0) + # compute angle + t = np.arctan2(points[:,0]-center[0], points[:,1]-center[1]) + sort_t = np.sort(t) + t = list(t) + u = [t.index(element) for element in sort_t] + ordered_points = points[u] + return ordered_points + else: + raise ValueError('The argument has to be a Nx3 numpy array') + + +@calcfunction +def get_absolute_chemical_potential(relative_chemical_potential, ref_energy): + ref_energy = ref_energy.get_dict() + relative_chemical_potential = relative_chemical_potential.get_dict() + relative_chempot = Dict_to_pandas_df(relative_chemical_potential) + + absolute_chemical_potential = {} + for element in relative_chempot.columns: + absolute_chemical_potential[element] = ref_energy[element] + np.array(relative_chempot[element]) + + return Dict(absolute_chemical_potential) + +@calcfunction +def get_StabilityData(matrix_eqns, stability_vertices, compound, dependent_element): + + # from aiida_defects.data.data import StabilityData + from aiida.plugins import DataFactory + + M = matrix_eqns.get_dict() + vertices = stability_vertices.get_dict() + + StabilityData = DataFactory('array.stability') + stability_region = StabilityData() + stability_region.set_data(np.array(M['data']), M['index'], M['column'], np.array(vertices['data']), compound.value, dependent_element.value) + + return stability_region diff --git a/aiida_defects/formation_energy/corrections/gaussian_countercharge/gaussian_countercharge.py b/aiida_defects/formation_energy/corrections/gaussian_countercharge/gaussian_countercharge.py index 3472bf4..cbbecbd 100644 --- a/aiida_defects/formation_energy/corrections/gaussian_countercharge/gaussian_countercharge.py +++ b/aiida_defects/formation_energy/corrections/gaussian_countercharge/gaussian_countercharge.py @@ -7,16 +7,15 @@ ######################################################################################## from __future__ import absolute_import -from aiida.engine import WorkChain, calcfunction, ToContext, while_ +from aiida.engine import WorkChain, calcfunction, ToContext, while_, if_ from aiida import orm from aiida_defects.formation_energy.potential_alignment.potential_alignment import PotentialAlignmentWorkchain from .model_potential.model_potential import ModelPotentialWorkchain -from aiida_defects.formation_energy.potential_alignment.utils import get_potential_difference -from aiida_defects.formation_energy.corrections.gaussian_countercharge.utils import get_total_correction, get_total_alignment - -from .utils import fit_energies, calc_correction - +from aiida_defects.formation_energy.potential_alignment.utils import get_potential_difference, get_interpolation +from .utils import get_total_correction, get_charge_model_fit, fit_energies, calc_correction, is_isotrope +from qe_tools import CONSTANTS +import numpy as np class GaussianCounterChargeWorkchain(WorkChain): """ @@ -29,66 +28,128 @@ class GaussianCounterChargeWorkchain(WorkChain): @classmethod def define(cls, spec): super(GaussianCounterChargeWorkchain, cls).define(spec) - spec.input("v_host", valid_type=orm.ArrayData) - spec.input("v_defect_q0", valid_type=orm.ArrayData) - spec.input("v_defect_q", valid_type=orm.ArrayData) - spec.input("defect_charge", valid_type=orm.Float) + + spec.input("host_structure", + valid_type=orm.StructureData, + help="The structure of the host system.") + spec.input("defect_charge", + valid_type=orm.Float, + help="The target defect charge state.") spec.input("defect_site", - valid_type=orm.List, - help="Defect site position in crystal coordinates") - spec.input("host_structure", valid_type=orm.StructureData) + valid_type=orm.List, + help="Defect site position in crystal coordinates.") spec.input("epsilon", - valid_type=orm.Float, - help="Dielectric constant for the host material") + valid_type=orm.ArrayData, + help="Dielectric tensor (3x3) for the host material. The name of the array in epsilon") spec.input("model_iterations_required", - valid_type=orm.Int, - default=orm.Int(3)) + valid_type=orm.Int, + default=lambda: orm.Int(3), + help="The number of model charge systems to compute. More may improve convergence.") spec.input("cutoff", - valid_type=orm.Float, - default=orm.Float(40.), - help="Plane wave cutoff for electrostatic model") + valid_type=orm.Float, + default=lambda: orm.Float(40.), + help="Plane wave cutoff for electrostatic model.") + spec.input("v_host", + valid_type=orm.ArrayData, + help="The electrostatic potential of the host system (in eV).") + spec.input("v_defect_q0", + valid_type=orm.ArrayData, + help="The electrostatic potential of the defect system in the 0 charge state (in eV).") + spec.input("v_defect_q", + valid_type=orm.ArrayData, + help="The electrostatic potential of the defect system in the target charge state (in eV).") + spec.input("rho_host", + valid_type=orm.ArrayData, + help="The charge density of the host system.") + spec.input("rho_defect_q", + valid_type=orm.ArrayData, + help="The charge density of the defect system in the target charge state.") + + # Charge Model Settings + spec.input_namespace('charge_model', + help="Namespace for settings related to different charge models") + spec.input("charge_model.model_type", + valid_type=orm.Str, + help="Charge model type: 'fixed' or 'fitted'", + default=lambda: orm.Str('fixed')) + # Fixed + spec.input_namespace('charge_model.fixed', required=False, populate_defaults=False, + help="Inputs for a fixed charge model using a user-specified multivariate gaussian") + # spec.input("charge_model.fixed.gaussian_params", + # valid_type=orm.List, + # help="A length 9 list of parameters needed to construct the " + # "gaussian charge distribution. The format required is " + # "[x0, y0, z0, sigma_x, sigma_y, sigma_z, cov_xy, cov_xz, cov_yz]") + spec.input("charge_model.fixed.covariance_matrix", + valid_type=orm.ArrayData, + help="The covariance matrix used to construct the gaussian charge distribution.") + # "gaussian charge distribution. The format required is " + # "[x0, y0, z0, sigma_x, sigma_y, sigma_z, cov_xy, cov_xz, cov_yz]") + + # Fitted + spec.input_namespace('charge_model.fitted', required=False, populate_defaults=False, + help="Inputs for a fitted charge model using a multivariate anisotropic gaussian.") + spec.input("charge_model.fitted.tolerance", + valid_type=orm.Float, + help="Permissable error for any fitted charge model parameter.", + default=lambda: orm.Float(1.0e-3)) + spec.input("charge_model.fitted.strict_fit", + valid_type=orm.Bool, + help="When true, exit the workchain if a fitting parameter is outside the specified tolerance.", + default=lambda: orm.Bool(True)) spec.outline( cls.setup, + if_(cls.should_fit_charge)( + cls.fit_charge_model, + ), while_(cls.should_run_model)( cls.compute_model_potential, ), cls.check_model_potential_workchains, + cls.get_isolated_energy, cls.compute_dft_difference_potential, cls.submit_alignment_workchains, cls.check_alignment_workchains, - cls.get_isolated_energy, + #cls.get_isolated_energy, cls.get_model_corrections, cls.compute_correction, ) - spec.output('v_dft_difference', valid_type=orm.ArrayData) - spec.output('alignment_q0_to_host', valid_type=orm.Float) - spec.output('alignment_dft_to_model', valid_type=orm.Float) - spec.output('total_alignment', valid_type=orm.Float, required=True) + spec.output('gaussian_parameters', valid_type=orm.Dict, required=False) +# spec.output('v_dft_difference', valid_type=orm.ArrayData) +# spec.output('alignment_q0_to_host', valid_type=orm.Float) +# spec.output('alignment_diff_q_q0_to_model', valid_type=orm.Float) +# spec.output('alignment_diff_q_host_to_model', valid_type=orm.Float) + spec.output('potential_alignment', valid_type=orm.Float, required=True) spec.output('total_correction', valid_type=orm.Float) spec.output('electrostatic_correction', valid_type=orm.Float) # spec.output('isolated_energy', valid_type=orm.Float, required=True) # Not sure if anyone would use this - # spec.output('model_correction_energies', valid_type=orm.Dict, required=True) # Again, not sure if useful - spec.exit_code( - 401, + # spec.output('model_correction_energies', valid_type=orm.Dict, required=True) + + spec.exit_code(201, 'ERROR_INVALID_INPUT_ARRAY', message='the input ArrayData object can only contain one array') - spec.exit_code( - 409, + spec.exit_code(202, + 'ERROR_BAD_INPUT_ITERATIONS_REQUIRED', + message='The required number of iterations must be at least 3') + spec.exit_code(203, + 'ERROR_INVALID_CHARGE_MODEL', + message='the charge model type is not known') + spec.exit_code(204, + 'ERROR_BAD_INPUT_CHARGE_MODEL_PARAMETERS', + message='Only the parameters relating to the chosen charge model should be specified') + spec.exit_code(301, 'ERROR_SUB_PROCESS_FAILED_ALIGNMENT', message='the electrostatic potentials could not be aligned') - spec.exit_code( - 413, + spec.exit_code(302, 'ERROR_SUB_PROCESS_FAILED_MODEL_POTENTIAL', message='The model electrostatic potential could not be computed') - spec.exit_code( - 410, + spec.exit_code(303, 'ERROR_SUB_PROCESS_FAILED_FINAL_SCF', message='the final scf PwBaseWorkChain sub process failed') - spec.exit_code( - 411, - 'ERROR_BAD_INPUT_ITERATIONS_REQUIRED', - message='The required number of iterations must be at least 3') + spec.exit_code(304, + 'ERROR_BAD_CHARGE_FIT', + message='the mode fit to charge density is exceeds tolerances') def setup(self): @@ -97,10 +158,38 @@ def setup(self): """ ## Verification - if self.inputs.model_iterations_required < 3: - self.report('The requested number of iterations, {}, is too low. At least 3 are required to achieve an #adequate data fit'.format(self.inputs.model_iterations_required.value)) + # Minimum number of iterations required. + # TODO: Replace this with an input ports validator + if self.inputs.charge_model.model_type == 'fitted' and self.inputs.model_iterations_required < 3: + self.report('The requested number of iterations, {}, is too low. At least 3 are required to achieve an adequate data fit'.format(self.inputs.model_iterations_required.value)) return self.exit_codes.ERROR_BAD_INPUT_ITERATIONS_REQUIRED + # Check if charge model scheme is valid: + model_schemes_available = ["fixed", "fitted"] + self.ctx.charge_model = self.inputs.charge_model.model_type + if self.ctx.charge_model not in model_schemes_available: + return self.exit_codes.ERROR_INVALID_CHARGE_MODEL + + # Check if required charge model namespace is specified + # TODO: Replace with input ports validator + if self.ctx.charge_model == 'fitted': + if 'fitted' not in self.inputs.charge_model: #Wanted fitted, but no params given + return self.exit_codes.ERROR_BAD_INPUT_CHARGE_MODEL_PARAMETERS + elif 'fixed' in self.inputs.charge_model: #Wanted fitted, but gave fixed params + return self.exit_codes.ERROR_BAD_INPUT_CHARGE_MODEL_PARAMETERS + elif self.ctx.charge_model == 'fixed': + self.ctx.is_model_isotrope = False + # check if the gaussian parameters correspond to an isotropic gaussian + if is_isotrope(self.inputs.charge_model.fixed.covariance_matrix.get_array('sigma')) and is_isotrope(self.inputs.epsilon.get_array('epsilon')): + self.report('DEBUG: the given gaussian charge distribution and dielectric constant are isotropic') + self.inputs.model_iterations_required = orm.Int(1) + self.ctx.is_model_isotrope = True + self.ctx.sigma = np.mean(np.diag(self.inputs.charge_model.fixed.covariance_matrix.get_array('sigma'))) + if 'fixed' not in self.inputs.charge_model: #Wanted fixed, but no params given + return self.exit_codes.ERROR_BAD_INPUT_CHARGE_MODEL_PARAMETERS + elif 'fitted' in self.inputs.charge_model: #Wanted fixed, but gave fitted params + return self.exit_codes.ERROR_BAD_INPUT_CHARGE_MODEL_PARAMETERS + # Track iteration number self.ctx.model_iteration = orm.Int(0) @@ -132,6 +221,43 @@ def setup(self): return + + def should_fit_charge(self): + """ + Return whether the charge model should be fitted + """ + return (self.ctx.charge_model == 'fitted') + + + def fit_charge_model(self): + """ + Fit an anisotropic gaussian to the charge state electron density + """ + + fit = get_charge_model_fit( + self.inputs.rho_host, + self.inputs.rho_defect_q, + self.inputs.host_structure) + + self.ctx.fitted_params = orm.List(list=fit['fit']) + self.ctx.peak_charge = orm.Float(fit['peak_charge']) + self.out('gaussian_parameters', fit) + self.report('DEBUG: the gaussian parameters obtained from fitting are: {}'.format(fit['fit'])) + + for parameter in fit['error']: + if parameter > self.inputs.charge_model.fitted.tolerance: + self.logger.warning("Charge fitting parameter worse than allowed tolerance") + if self.inputs.charge_model.fitted.strict_fit: + return self.exit_codes.ERROR_BAD_CHARGE_FIT + + if is_gaussian_isotrope(self.ctx.fitted_params.get_list()[3:]) and is_isotrope(self.inputs.epsilon.get_array('epsilon')): + self.report('The fitted gaussian and the dielectric constant are isotropic. The isolated model energy will be computed analytically') + self.inputs.model_iterations_required = orm.Int(1) + self.ctx.is_model_isotrope = True + self.ctx.sigma = np.mean(self.ctx.fitted_params.get_list()[3:6]) + else: + self.ctx.is_model_isotrope = False + def should_run_model(self): """ Return whether a model workchain should be run, which is dependant on the number of model energies computed @@ -139,6 +265,7 @@ def should_run_model(self): """ return self.ctx.model_iteration < self.inputs.model_iterations_required + def compute_model_potential(self): """ Compute the potential for the system using a model charge distribution @@ -149,21 +276,34 @@ def compute_model_potential(self): self.report("Computing model potential for scale factor {}".format( scale_factor.value)) + if self.ctx.charge_model == 'fitted': + gaussian_params = self.ctx.fitted_params + peak_charge = self.ctx.peak_charge + else: + # gaussian_params = self.inputs.charge_model.fixed.gaussian_params + cov_mat = self.inputs.charge_model.fixed.covariance_matrix.get_array('sigma') + params = self.inputs.defect_site.get_list()+list(np.diag(cov_mat))+list(cov_mat[0, 1:])+[cov_mat[1,2]] + gaussian_params = orm.List(list=params) + peak_charge = orm.Float(0.) + inputs = { + 'peak_charge': peak_charge, 'defect_charge': self.inputs.defect_charge, 'scale_factor': scale_factor, 'host_structure': self.inputs.host_structure, 'defect_site': self.inputs.defect_site, 'cutoff': self.inputs.cutoff, 'epsilon': self.inputs.epsilon, + 'gaussian_params' : gaussian_params } workchain_future = self.submit(ModelPotentialWorkchain, **inputs) label = 'model_potential_scale_factor_{}'.format(scale_factor.value) self.to_context(**{label: workchain_future}) + def check_model_potential_workchains(self): """ - Check if the model potential alignment workchains have finished correctly. + Check if the model potential workchains have finished correctly. If yes, assign the outputs to the context """ for ii in range(self.inputs.model_iterations_required.value): @@ -173,24 +313,41 @@ def check_model_potential_workchains(self): if not model_workchain.is_finished_ok: self.report( 'Model potential workchain for scale factor {} failed with status {}' - .format(model_workchain.scale_factor, + .format(scale_factor, model_workchain.exit_status)) return self.exit_codes.ERROR_SUB_PROCESS_FAILED_MODEL_POTENTIAL else: if scale_factor == 1: self.ctx.v_model = model_workchain.outputs.model_potential - self.ctx.model_energies[str( - scale_factor)] = model_workchain.outputs.model_energy - self.ctx.model_structures[str( - scale_factor)] = model_workchain.outputs.model_structure + self.ctx.charge_model = model_workchain.outputs.model_charge + self.ctx.model_energies[str(scale_factor)] = model_workchain.outputs.model_energy + self.ctx.model_structures[str(scale_factor)] = model_workchain.outputs.model_structure + def compute_dft_difference_potential(self): """ Compute the difference in the DFT potentials for the cases of q=q and q=0 """ - self.ctx.v_defect_q_q0 = get_potential_difference( - self.inputs.v_defect_q, self.inputs.v_defect_q0) - self.out('v_dft_difference', self.ctx.v_defect_q_q0) + #self.ctx.v_defect_q_q0 = get_potential_difference( + # self.inputs.v_defect_q, self.inputs.v_defect_q0) + #self.out('v_dft_difference', self.ctx.v_defect_q_q0) + + first_array_shape = self.inputs.v_defect_q.get_array('data').shape + second_array_shape = self.inputs.v_host.get_array('data').shape + # second_array_shape = self.inputs.v_defect_q0.get_array('data').shape + if first_array_shape != second_array_shape: + target_shape = orm.List(list=np.max(np.vstack((first_array_shape, second_array_shape)), axis=0).tolist()) + first_array = get_interpolation(self.inputs.v_defect_q, target_shape)#.get_array('interpolated_array') + second_array = get_interpolation(self.inputs.v_host, target_shape)#.get_array('interpolated_array') + # second_array = get_interpolation(self.inputs.v_defect_q0, target_shape) + self.ctx.v_defect_q_host = get_potential_difference(first_array, second_array) + else: + self.ctx.v_defect_q_host = get_potential_difference( + self.inputs.v_defect_q, self.inputs.v_host) + # self.ctx.v_defect_q_host = get_potential_difference( + # self.inputs.v_defect_q, self.inputs.v_defect_q0) + #self.out('v_dft_difference', self.ctx.v_defect_q_q0) + def submit_alignment_workchains(self): """ @@ -198,24 +355,56 @@ def submit_alignment_workchains(self): state with the pristine host system """ - # Compute the alignment between the defect, in q=0, and the host + # # Compute the alignment between the defect, in q=0, and the host + # inputs = { + # "allow_interpolation": orm.Bool(True), + # "mae":{ + # "first_potential": self.inputs.v_defect_q0, + # "second_potential": self.inputs.v_host, + # "defect_site": self.inputs.defect_site + # }, + # } + + # workchain_future = self.submit(PotentialAlignmentWorkchain, **inputs) + # label = 'workchain_alignment_q0_to_host' + # self.to_context(**{label: workchain_future}) + + # Convert units from from eV in model potential to Ryd unit as in DFT potential, and also change sign + # The potential alignment has to be converted back to eV. It is done in the mae/utils.py. Not pretty, has to be cleaned + # TODO: Check if this breaks provenance graph + # v_model = orm.ArrayData() + # # v_model.set_array('data', + # # self.ctx.v_model.get_array(self.ctx.v_model.get_arraynames()[0])/(-1.0*CONSTANTS.ry_to_ev)) # eV to Ry unit of potential - This is dirty - need to harmonise units + # v_model.set_array('data', + # self.ctx.v_model.get_array(self.ctx.v_model.get_arraynames()[0])*-2.0) # Hartree to Ry unit of potential - This is dirty - need to harmonise units + + # # Compute the alignment between the difference of DFT potentials v_q and v_q0, and the model + # inputs = { + + # "allow_interpolation": orm.Bool(True), + # "mae":{ + # "first_potential": self.ctx.v_defect_q_q0, + # # "second_potential": v_model, + # "second_potential": self.ctx.v_model, + # "defect_site": self.inputs.defect_site + # }, + # } + # workchain_future = self.submit(PotentialAlignmentWorkchain, **inputs) + # label = 'workchain_alignment_q-q0_to_model' + # self.to_context(**{label: workchain_future}) + + # Compute the alignment between the difference of DFT potentials v_q and v_host, and the model inputs = { - "first_potential": self.inputs.v_defect_q0, - "second_potential": self.inputs.v_host - } - workchain_future = self.submit(PotentialAlignmentWorkchain, **inputs) - label = 'workchain_alignment_q0_to_host' - self.to_context(**{label: workchain_future}) - # Compute the alignment between the defect DFT difference potential, and the model - inputs = { - "first_potential": self.ctx.v_defect_q_q0, - "second_potential": self.ctx.v_model, - "interpolate": - orm.Bool(True) # This will more or less always be required + "allow_interpolation": orm.Bool(True), + "mae":{ + "first_potential": self.ctx.v_defect_q_host, + "second_potential": self.ctx.v_model, + "defect_site": self.inputs.defect_site + }, } workchain_future = self.submit(PotentialAlignmentWorkchain, **inputs) - label = 'workchain_alignment_dft_to_model' + label = 'workchain_alignment_q-host_to_model' self.to_context(**{label: workchain_future}) def check_alignment_workchains(self): @@ -224,47 +413,70 @@ def check_alignment_workchains(self): If yes, assign the outputs to the context """ - # q0 to host - alignment_wc = self.ctx['workchain_alignment_q0_to_host'] + # # q0 to host + # alignment_wc = self.ctx['workchain_alignment_q0_to_host'] + # if not alignment_wc.is_finished_ok: + # self.report( + # 'Potential alignment workchain (defect q=0 to host) failed with status {}' + # .format(alignment_wc.exit_status)) + # return self.exit_codes.ERROR_SUB_PROCESS_FAILED_ALIGNMENT + # else: + # self.ctx.alignment_q0_to_host = alignment_wc.outputs.alignment_required + + # # DFT q-q0 to model + # alignment_wc = self.ctx['workchain_alignment_q-q0_to_model'] + # if not alignment_wc.is_finished_ok: + # self.report( + # 'Potential alignment workchain (DFT q-q0 to model) failed with status {}' + # .format(alignment_wc.exit_status)) + # return self.exit_codes.ERROR_SUB_PROCESS_FAILED_ALIGNMENT + # else: + # self.ctx['alignment_q-q0_to_model'] = alignment_wc.outputs.alignment_required + + # DFT q-host to model + alignment_wc = self.ctx['workchain_alignment_q-host_to_model'] if not alignment_wc.is_finished_ok: self.report( - 'Potential alignment workchain (defect q=0 to host) failed with status {}' + 'Potential alignment workchain (DFT q-host to model) failed with status {}' .format(alignment_wc.exit_status)) return self.exit_codes.ERROR_SUB_PROCESS_FAILED_ALIGNMENT else: - self.ctx.alignment_q0_to_host = alignment_wc.outputs.alignment_required - - # DFT diff to model - alignment_wc = self.ctx['workchain_alignment_dft_to_model'] - if not alignment_wc.is_finished_ok: - self.report( - 'Potential alignment workchain (DFT diff to model) failed with status {}' - .format(alignment_wc.exit_status)) - return self.exit_codes.ERROR_SUB_PROCESS_FAILED_ALIGNMENT - else: - self.ctx.alignment_dft_to_model = alignment_wc.outputs.alignment_required + self.ctx['alignment_q-host_to_model'] = alignment_wc.outputs.alignment_required def get_isolated_energy(self): """ Fit the calculated model energies and obtain an estimate for the isolated model energy """ - # Get the linear dimensions of the structures - linear_dimensions = {} + if not self.ctx.is_model_isotrope: + # Get the linear dimensions of the structures + linear_dimensions = {} - for scale, structure in self.ctx.model_structures.items(): - volume = structure.get_cell_volume() - linear_dimensions[scale] = 1 / (volume**(1 / 3.)) + for scale, structure in self.ctx.model_structures.items(): + volume = structure.get_cell_volume() + linear_dimensions[scale] = 1 / (volume**(1 / 3.)) + + self.report( + "Fitting the model energies to obtain the model energy for the isolated case" + ) + self.ctx.isolated_energy = fit_energies( + orm.Dict(dict=linear_dimensions), + orm.Dict(dict=self.ctx.model_energies)) + else: + sigma = self.ctx.sigma + defect_charge = self.inputs.defect_charge.value + # # Epsilon is now expected to be a tensor, and so to get a scalar here we diagonalise. + # epsilon_tensor = self.inputs.epsilon.get_array('epsilon') + epsilon = np.mean(np.diag(self.inputs.epsilon.get_array('epsilon'))) + self.report( + "Computing the energy of the isolated gaussian analytically" + ) + self.ctx.isolated_energy = orm.Float(defect_charge**2/(2*epsilon*sigma*np.sqrt(np.pi))*CONSTANTS.hartree_to_ev) - self.report( - "Fitting the model energies to obtain the model energy for the isolated case" - ) - self.ctx.isolated_energy = fit_energies( - orm.Dict(dict=linear_dimensions), - orm.Dict(dict=self.ctx.model_energies)) self.report("The isolated model energy is {} eV".format( self.ctx.isolated_energy.value)) + def get_model_corrections(self): """ Get the energy corrections for each model size @@ -275,23 +487,27 @@ def get_model_corrections(self): self.ctx.model_correction_energies[scale_factor] = calc_correction( self.ctx.isolated_energy, model_energy) + def compute_correction(self): """ Compute the Gaussian Countercharge correction """ electrostatic_correction = self.ctx.model_correction_energies['1'] + potential_alignment = self.ctx['alignment_q-host_to_model'] - total_alignment = get_total_alignment(self.ctx.alignment_dft_to_model, - self.ctx.alignment_q0_to_host, - self.inputs.defect_charge) + # total_alignment = get_total_alignment(self.ctx['alignment_q-q0_to_model'], + # self.ctx['alignment_q0_to_host'], + # self.inputs.defect_charge) + # total_alignment = get_alignment(self.ctx['alignment_q-host_to_model'], self.inputs.defect_charge) total_correction = get_total_correction(electrostatic_correction, - total_alignment) + potential_alignment, + self.inputs.defect_charge) - self.report('The computed total alignment is {} eV'.format( - total_alignment.value)) - self.out('total_alignment', total_alignment) + self.report('The computed potential alignment is {} eV'.format( + potential_alignment.value)) + self.out('potential_alignment', potential_alignment) self.report('The computed electrostatic correction is {} eV'.format( electrostatic_correction.value)) @@ -303,7 +519,8 @@ def compute_correction(self): self.out('total_correction', total_correction) # Store additional outputs - self.out('alignment_q0_to_host', self.ctx.alignment_q0_to_host) - self.out('alignment_dft_to_model', self.ctx.alignment_dft_to_model) + # self.out('alignment_q0_to_host', self.ctx.alignment_q0_to_host) + # self.out('alignment_diff_q_q0_to_model', self.ctx['alignment_q-q0_to_model']) + # self.out('alignment_diff_q_host_to_model', self.ctx['alignment_q-host_to_model']) self.report('Gaussian Countercharge workchain completed successfully') diff --git a/aiida_defects/formation_energy/corrections/gaussian_countercharge/model_potential/model_potential.py b/aiida_defects/formation_energy/corrections/gaussian_countercharge/model_potential/model_potential.py index 23551f7..46f741d 100644 --- a/aiida_defects/formation_energy/corrections/gaussian_countercharge/model_potential/model_potential.py +++ b/aiida_defects/formation_energy/corrections/gaussian_countercharge/model_potential/model_potential.py @@ -11,7 +11,7 @@ from aiida import orm from aiida.engine import WorkChain, calcfunction, while_ -from qe_tools.constants import bohr_to_ang +from qe_tools import CONSTANTS from .utils import (create_model_structure, get_cell_matrix, get_reciprocal_cell, get_reciprocal_grid, get_charge_model, @@ -25,16 +25,35 @@ class ModelPotentialWorkchain(WorkChain): @classmethod def define(cls, spec): super(ModelPotentialWorkchain, cls).define(spec) - spec.input('defect_charge', valid_type=orm.Float) - spec.input('scale_factor', valid_type=orm.Int) - spec.input('host_structure', valid_type=orm.StructureData) + spec.input("defect_charge", + valid_type=orm.Float, + help="The target defect charge state") + spec.input('scale_factor', + valid_type=orm.Int, + help="Scale factor to apply when constructing the model system") + spec.input('host_structure', + valid_type=orm.StructureData, + help="The unscaled host structure") spec.input('defect_site', - valid_type=orm.List, - help="Defect site position in crystal coordinates") - spec.input('cutoff', valid_type=orm.Float, default=orm.Float(40.)) + valid_type=orm.List, + help="Defect site position in crystal coordinates") + spec.input('cutoff', + valid_type=orm.Float, + default=lambda: orm.Float(40.), + help="Energy cutoff for grids in Rydberg") spec.input('epsilon', - valid_type=orm.Float, - help="Dielectric constant of the host material") + valid_type=orm.ArrayData, + help="Dielectric tensor (3x3) of the host material") + spec.input('gaussian_params', + valid_type=orm.List, + help="A length 9 list of parameters needed to construct the " + "gaussian charge distribution. The format required is " + "[x0, y0, z0, sigma_x, sigma_y, sigma_z, cov_xy, cov_xz, cov_yz]") + spec.input('peak_charge', + valid_type=orm.Float, + default=lambda: orm.Float(0.0), + help="Peak charge of the defect charge density distribution. If set to zero, no scaling will be done.") + spec.outline( cls.setup, cls.get_model_structure, @@ -45,6 +64,7 @@ def define(cls, spec): ) #spec.expose_outputs(PwBaseWorkChain, exclude=('output_structure',)) spec.output('model_energy', valid_type=orm.Float, required=True) + spec.output('model_charge', valid_type=orm.ArrayData, required=True) spec.output('model_potential', valid_type=orm.ArrayData, required=True) spec.output('model_structure', valid_type=orm.StructureData, @@ -65,13 +85,14 @@ def get_model_structure(self): self.report("Generating model structure") self.ctx.model_structure = create_model_structure( self.inputs.host_structure, self.inputs.scale_factor) - # Get cell matricies - real_cell = get_cell_matrix(self.ctx.model_structure) - self.ctx.reciprocal_cell = get_reciprocal_cell(real_cell) - self.report("DEBUG: recip cell: {}".format(self.ctx.reciprocal_cell)) - limits = np.array(self.ctx.model_structure.cell_lengths) / bohr_to_ang + # Get cell matrices + self.ctx.real_cell = get_cell_matrix(self.ctx.model_structure) + self.ctx.reciprocal_cell = get_reciprocal_cell(self.ctx.real_cell) +# self.report("DEBUG: recip cell: {}".format(self.ctx.reciprocal_cell)) + limits = np.array(self.ctx.model_structure.cell_lengths) / CONSTANTS.bohr_to_ang self.ctx.limits = orm.List(list=limits.tolist()) + def compute_model_charge(self): """ Compute the model charge distribution @@ -84,13 +105,21 @@ def compute_model_charge(self): self.ctx.grid_dimensions = get_reciprocal_grid( self.ctx.reciprocal_cell, self.inputs.cutoff.value) + self.ctx.cell_matrix = orm.ArrayData() + self.ctx.cell_matrix.set_array('cell_matrix', self.ctx.real_cell) + + if self.inputs.peak_charge == 0.0: + peak_charge = None + else: + peak_charge = self.inputs.peak_charge + self.ctx.charge_model = get_charge_model( - charge=self.inputs.defect_charge, - dimensions=self.ctx.grid_dimensions, - limits=self.ctx.limits, - sigma=orm.Float( - 2.614), #TODO: Automated fitting/3-tuple of sigma values - defect_position=self.inputs.defect_site) + cell_matrix = self.ctx.cell_matrix, + peak_charge = peak_charge, + defect_charge = self.inputs.defect_charge, + dimensions = self.ctx.grid_dimensions, + gaussian_params = self.inputs.gaussian_params + ) def compute_model_potential(self): """ @@ -116,12 +145,10 @@ def compute_energy(self): self.report("Computing model energy for scale factor {}".format( self.inputs.scale_factor.value)) - self.report("DEBUG: type {}".format(type(self.ctx.model_potential))) self.ctx.model_energy = get_energy( potential=self.ctx.model_potential, charge_density=self.ctx.charge_model, - dimensions=self.ctx.grid_dimensions, - limits=self.ctx.limits) + cell_matrix=self.ctx.cell_matrix) self.report("Calculated model energy: {} eV".format( self.ctx.model_energy.value)) @@ -132,6 +159,7 @@ def results(self): """ # Return the model potential for the cell which corresponds to the host structure self.out('model_energy', self.ctx.model_energy) + self.out('model_charge', self.ctx.charge_model) self.out('model_potential', self.ctx.model_potential) self.out('model_structure', self.ctx.model_structure) self.report("Finished successfully") diff --git a/aiida_defects/formation_energy/corrections/gaussian_countercharge/model_potential/utils.py b/aiida_defects/formation_energy/corrections/gaussian_countercharge/model_potential/utils.py index 31cbef7..d51bd2b 100644 --- a/aiida_defects/formation_energy/corrections/gaussian_countercharge/model_potential/utils.py +++ b/aiida_defects/formation_energy/corrections/gaussian_countercharge/model_potential/utils.py @@ -9,11 +9,11 @@ import numpy as np from scipy.optimize import curve_fit +from scipy.stats import multivariate_normal from aiida import orm from aiida.engine import calcfunction -from qe_tools.constants import hartree_to_ev - +from qe_tools import CONSTANTS @calcfunction @@ -43,8 +43,7 @@ def get_cell_matrix(structure): 3x3 cell matrix array in units of Bohr """ - from qe_tools.constants import bohr_to_ang - cell_matrix = np.array(structure.cell) / bohr_to_ang # Angstrom to Bohr + cell_matrix = np.array(structure.cell) / CONSTANTS.bohr_to_ang # Angstrom to Bohr return cell_matrix @@ -63,9 +62,7 @@ def get_reciprocal_cell(cell_matrix): 3x3 cell matrix array in reciprocal units """ from numpy.linalg import inv - #reciprocal_cell = (inv(cell_matrix)).transpose() - reciprocal_cell = (2 * np.pi * inv(cell_matrix) - ).transpose() # Alternative definition (2pi) + reciprocal_cell = (2 * np.pi * inv(cell_matrix)).transpose() # Alternative definition (2pi) return reciprocal_cell @@ -93,7 +90,7 @@ def get_reciprocal_grid(cell_matrix, cutoff): G_max = 2.0 * np.sqrt(cutoff) # Ry # Get the number of G-vectors needed along each cell vector - # Note, casting to int alway rounds down so we add one + # Note, casting to int always rounds down so we add one grid_max = (G_max / np.linalg.norm(cell_matrix, axis=1)).astype(int) + 1 # For convenience later, ensure the grid is odd valued @@ -104,68 +101,169 @@ def get_reciprocal_grid(cell_matrix, cutoff): return orm.List(list=grid_max.tolist()) +def get_xyz_coords(cell_matrix, dimensions): + """ + For a given array, generate an array of xyz coordinates in the cartesian basis + """ + + # Generate a grid of crystal coordinates + i = np.linspace(0., 1., dimensions[0]) + j = np.linspace(0., 1., dimensions[1]) + k = np.linspace(0., 1., dimensions[2]) + # Generate NxN arrays of crystal coords + iii, jjj, kkk = np.meshgrid(i, j, k, indexing='ij') + # Flatten this to a 3xNN array + ijk_array = np.array([iii.ravel(), jjj.ravel(), kkk.ravel()]) + # Change the crystal basis to a cartesian basis + xyz_array = np.dot(cell_matrix.T, ijk_array) + + return xyz_array + + +def generate_charge_model(cell_matrix, peak_charge=None): + """ + Return a function to compute a periodic gaussian on a grid. + The returned function can be used for fitting. + + Parameters + ---------- + cell_matrix: 3x3 array + Cell matrix of the real space cell + peak_charge: float + The peak charge density at the centre of the gaussian. + Used for scaling the result. + + Returns + ------- + compute_charge + A function that will compute a periodic gaussian on a grid + for a given cell and peak charge intensity + """ + + def compute_charge( + xyz_real, + x0, y0, z0, + sigma_x, sigma_y, sigma_z, + cov_xy, cov_xz, cov_yz): + """ + For a given system charge, create a model charge distribution using + an anisotropic periodic 3D gaussian. + The charge model for now is a Gaussian. + + NOTE: + The values for sigma and cov are not the values used in construction + of the Gaussian. After the covariance matrix is constructed, its + transpose is multiplied by itself (that is to construct a Gram matrix) + to ensure that it is positive-semidefinite. It is this matrix which is + the real covariance matrix. This transformation is to allow this + function to be used directly by the fitting algorithm without a danger + of crashing. + + Parameters + ---------- + xyz_real: 3xN array + Coordinates to compute the Gaussian for in cartesian coordinates. + x0, y0, z0: float + Center of the Gaussian in crystal coordinates. + sigma_x, sigma_y, sigma_z: float + Spread of the Gaussian (not the real values used, see note above). + cov_xy, cov_xz, cov_yz: float + Covariance values controlling the rotation of the Gaussian + (not the real values used, see note above). + + Returns + ------- + g + Values of the Gaussian computed at all of the desired coordinates and + scaled by the value of charge_integral. + + """ + + # Construct the pseudo-covariance matrix + V = np.array([[sigma_x, cov_xy, cov_xz],[cov_xy, sigma_y, cov_yz], [cov_xz, cov_yz, sigma_z]]) + # Construct the actual covariance matrix in a way that is always positive semi-definite + covar = np.dot(V.T, V) + + gauss_position = np.array([x0, y0, z0]) + + # Apply periodic boundary conditions + g = 0 + for ii in [-1, 0, 1]: + for jj in [-1, 0, 1]: + for kk in [-1, 0, 1]: + # Compute the periodic origin in crystal coordinates + origin_crystal = (gauss_position + np.array([ii, jj, kk])).reshape(3,1) + # Convert this to cartesian coordinates + origin_real = np.dot(cell_matrix.T, origin_crystal) + # Compute the Gaussian centred at this position + g = g + get_gaussian_3d(xyz_real.T, origin_real, covar) + + print("DEBUG: Integrated charge density (unscaled) = {}".format(get_integral(g, cell_matrix))) + + print("DEBUG: g.max() = {}".format(g.max())) + # Scale the result to match the peak charge density + if peak_charge: + g = g * (peak_charge / g.max()) + print("DEBUG: Peak Charge target = {}".format(peak_charge)) + print("DEBUG: Peak Charge scaled = {}".format(g.max())) + print("DEBUG: Integrated charge density (scaled) = {}".format(get_integral(g, cell_matrix))) + + return g + + return compute_charge + + @calcfunction -def get_charge_model(limits, - dimensions, - defect_position, - sigma=orm.Float(1.0), - charge=orm.Float(-1.0)): +def get_charge_model(cell_matrix, defect_charge, dimensions, gaussian_params, peak_charge=None): """ For a given system charge, create a model charge distribution. - The charge model for now is a Gaussian. - Grid = coord grid - TODO: Change of basis + + Parameters + ---------- + cell_matrix: 3x3 array + Cell matrix of the real space cell. + peak_charge : float + The peak charge density at the centre of the gaussian. + defect_charge : float + Charge state of the defect + dimensions: 3x1 array-like + Dimensions of grid to compute charge on. + gaussian_params: list (length 9) + Parameters determining the distribution position and shape obtained + by the fitting procedure. + + Returns + ------- + model_charge_array + The grid with the charge data as an AiiDA ArrayData object + """ - limits = limits.get_list() - dimensions = dimensions.get_list() - defect_position = defect_position.get_list() - sigma = sigma.value - charge = charge.value - print("DEBUG: Dimensions = {}".format(dimensions)) - print("DEBUG: Limits = {}".format(limits)) + cell_matrix = cell_matrix.get_array('cell_matrix') + if peak_charge: + peak_charge = peak_charge.value + defect_charge = defect_charge.value + dimensions = np.array(dimensions) + gaussian_params = gaussian_params.get_list() - i = np.linspace(0, limits[0], dimensions[0]) - j = np.linspace(0, limits[1], dimensions[1]) - k = np.linspace(0, limits[2], dimensions[2]) - grid = np.meshgrid(i, j, k, indexing='ij') + xyz_coords = get_xyz_coords(cell_matrix, dimensions) - # Get the gaussian at the defect position - g = get_gaussian_3d(grid, defect_position, sigma) + get_model = generate_charge_model(cell_matrix, peak_charge) + g = get_model(xyz_coords, *gaussian_params) - # Get the offsets - offsets = np.zeros(3) - for axis in range(3): - # Capture the offset needed for an image - if defect_position[axis] > limits[axis] / 2.0: - offsets[axis] = -limits[axis] - else: - offsets[axis] = +limits[axis] - - # Apply periodic boundary conditions - g = 0 - for dim0 in range(2): - for dim1 in range(2): - for dim2 in range(2): - image_offset = [dim0, dim1, dim2] * offsets - g = g + get_gaussian_3d( - grid, defect_position + image_offset, sigma=sigma) - - # Scale the charge density to the desired charge - #int_charge_density = np.trapz(np.trapz(np.trapz(g, i), j), k) - int_charge_density = get_integral(g, dimensions, limits) - print( - "DEBUG: Integrated charge density (g) = {}".format(int_charge_density)) - - g = g / (int_charge_density / charge) + # Unflatten the array + g = g.reshape(dimensions) - # Compensating jellium background - print("DEBUG: Integrated charge density (scaled_g) = {}".format( - get_integral(g, dimensions, limits))) + print("DEBUG: fit params: {}".format(gaussian_params)) - #scaled_g = scaled_g - np.sum(scaled_g)/np.prod(scaled_g.shape) - print("DEBUG: Integrated charge density (jellium) = {}".format( - get_integral(g, dimensions, limits))) + # Rescale to defect charge + print("DEBUG: Integrated charge density target = {}".format(defect_charge)) + g = g * (defect_charge / get_integral(g, cell_matrix)) + print("DEBUG: Integrated charge density (scaled) = {}".format(get_integral(g, cell_matrix))) + + # Compensating jellium background + # g = g - np.sum(g)/np.prod(g.shape) + # print("DEBUG: Integrated charge density (jellium) = {}".format(get_integral(g, cell_matrix))) # Pack the array model_charge_array = orm.ArrayData() @@ -174,29 +272,126 @@ def get_charge_model(limits, return model_charge_array -def get_gaussian_3d(grid, position, sigma): +# @calcfunction +# def get_charge_model_old(limits, +# dimensions, +# defect_position, +# sigma=orm.Float(1.0), +# charge=orm.Float(-1.0)): +# """ +# For a given system charge, create a model charge distribution. +# The charge model for now is a Gaussian. +# Grid = coord grid +# TODO: Change of basis +# """ +# limits = limits.get_list() +# dimensions = dimensions.get_list() +# defect_position = defect_position.get_list() +# sigma = sigma.value +# charge = charge.value + +# print("DEBUG: Dimensions = {}".format(dimensions)) +# print("DEBUG: Limits = {}".format(limits)) + +# i = np.linspace(0, limits[0], dimensions[0]) +# j = np.linspace(0, limits[1], dimensions[1]) +# k = np.linspace(0, limits[2], dimensions[2]) +# grid = np.meshgrid(i, j, k) + +# # Get the gaussian at the defect position +# g = get_gaussian_3d(grid, defect_position, sigma) + +# # Get the offsets +# offsets = np.zeros(3) +# for axis in range(3): +# # Capture the offset needed for an image +# if defect_position[axis] > limits[axis] / 2.0: +# offsets[axis] = -limits[axis] +# else: +# offsets[axis] = +limits[axis] + +# # Apply periodic boundary conditions +# g = 0 +# for dim0 in range(2): +# for dim1 in range(2): +# for dim2 in range(2): +# image_offset = [dim0, dim1, dim2] * offsets +# g = g + get_gaussian_3d( +# grid, defect_position + image_offset, sigma=sigma) + +# # Scale the charge density to the desired charge +# #int_charge_density = np.trapz(np.trapz(np.trapz(g, i), j), k) +# int_charge_density = get_integral(g, dimensions, limits) +# print( +# "DEBUG: Integrated charge density (g) = {}".format(int_charge_density)) + +# g = g / (int_charge_density / charge) + +# # Compensating jellium background +# print("DEBUG: Integrated charge density (scaled_g) = {}".format( +# get_integral(g, dimensions, limits))) + +# #scaled_g = scaled_g - np.sum(scaled_g)/np.prod(scaled_g.shape) +# print("DEBUG: Integrated charge density (jellium) = {}".format( +# get_integral(g, dimensions, limits))) + +# # Pack the array +# model_charge_array = orm.ArrayData() +# model_charge_array.set_array('model_charge', g) + +# return model_charge_array + +def get_gaussian_3d(grid, origin, covar): """ - Calculate 3D Gaussian on grid - NOTE: Minus sign at front give negative values of charge density throughout the cell + Compute anisotropic 3D Gaussian on grid + + Parameters + ---------- + grid: array + Array on which to compute gaussian + origin: array + Centre of gaussian + covar: 3x3 array + Covariance matrix of gaussian + + Returns + ------- + gaussian + anisotropic Gaussian on grid """ - x = grid[0] - position[0] - y = grid[1] - position[1] - z = grid[2] - position[2] - gaussian = -np.exp(-(x**2 + y**2 + z**2) / (2 * sigma**2)) / ( - (2.0 * np.pi)**1.5 * np.sqrt(sigma)) + origin = origin.ravel() + gaussian = multivariate_normal.pdf(grid, origin, covar) return gaussian -def get_integral(data, dimensions, limits): +# def get_gaussian_3d_old(grid, position, sigma): +# """ +# Calculate 3D Gaussian on grid +# NOTE: Minus sign at front give negative values of charge density throughout the cell +# """ +# x = grid[0] - position[0] +# y = grid[1] - position[1] +# z = grid[2] - position[2] + +# gaussian = -np.exp(-(x**2 + y**2 + z**2) / (2 * sigma**2)) / ( +# (2.0 * np.pi)**1.5 * np.sqrt(sigma)) + +# return gaussian + + +def get_integral(data, cell_matrix): """ - Get the integral of a uniformly spaced 3D data array + Get the integral of a uniformly spaced 3D data array by rectangular rule. + Works better than trapezoidal or Simpson's rule for sharpely peaked coarse grids. """ - limits = np.array(limits) - dimensions = np.array(dimensions) - volume_element = np.prod(limits / dimensions) - return np.sum(data) * volume_element + a = cell_matrix[0] + b = cell_matrix[1] + c = cell_matrix[2] + cell_vol = np.dot(np.cross(a, b), c) + element_volume = cell_vol / np.prod(data.shape) + return np.sum(data) * element_volume def get_fft(grid): @@ -226,8 +421,8 @@ def get_model_potential(cell_matrix, dimensions, charge_density, epsilon): The dimensions required for the reciprocal space grid charge_density: array The calculated model charge density on a 3-dimensional real space grid - epsilon: float - The value of the dielectric constant + epsilon: array + 3x3 dielectric tensor Returns ------- @@ -238,37 +433,41 @@ def get_model_potential(cell_matrix, dimensions, charge_density, epsilon): dimensions = np.array(dimensions) cell_matrix = cell_matrix.get_array('cell_matrix') charge_density = charge_density.get_array('model_charge') - epsilon = epsilon.value + epsilon = epsilon.get_array('epsilon') # Set up a reciprocal space grid for the potential # Prepare coordinates in a 3D array of ijk vectors # (Note: Indexing is column major order, but the 4th dimension vectors remain ijk) dimensions = dimensions // 2 #floor division - ijk_array = np.mgrid[-dimensions[0]:dimensions[0] + - 1, -dimensions[1]:dimensions[1] + - 1, -dimensions[2]:dimensions[2] + 1].T + ijk_array = np.mgrid[ + -dimensions[0]:dimensions[0] + 1, + -dimensions[1]:dimensions[1] + 1, + -dimensions[2]:dimensions[2] + 1].T # Get G vectors - G_array = np.dot( - ijk_array, - (cell_matrix.T - )) # To do: check why we use a grid that goes way past the recip cell + G_array = np.dot(ijk_array, (cell_matrix.T)) + G_array_shape = G_array.shape[0:3] # Drop last axis - we know that each vector is len 3 - # Calculate the square modulus - G_sqmod_array = (np.linalg.norm(G_array, axis=3)**2).T + # Compute permittivity for all g-vectors + dielectric = [] + for gvec in G_array.reshape(-1,3): + dielectric.append(gvec@epsilon@gvec.T) + dielectric = np.array(dielectric).reshape(G_array_shape).T # Get the reciprocal space charge density charge_density_g = get_fft(charge_density) # Compute the model potential v_model = np.divide( - charge_density_g, G_sqmod_array, where=G_sqmod_array != 0.0) - V_model_g = v_model * 4. * np.pi / epsilon - + charge_density_g, dielectric, where=dielectric != 0.0) + V_model_g = 4. * np.pi * v_model + + # Set the component G=0 to zero V_model_g[dimensions[0] + 1, dimensions[1] + 1, dimensions[2] + 1] = 0.0 # Get the model potential in real space + # V_model_r = get_inverse_fft(V_model_g) * CONSTANTS.hartree_to_ev V_model_r = get_inverse_fft(V_model_g) # Pack up the array @@ -278,17 +477,16 @@ def get_model_potential(cell_matrix, dimensions, charge_density, epsilon): return V_model_array -def get_energy(potential, charge_density, dimensions, limits): +@calcfunction +def get_energy(potential, charge_density, cell_matrix): """ Calculate the total energy for a given model potential """ - - potential = potential.get_array('model_potential') + cell_matrix = cell_matrix.get_array('cell_matrix') + # potential = potential.get_array('model_potential') + potential = potential.get_array('model_potential') * CONSTANTS.hartree_to_ev charge_density = charge_density.get_array('model_charge') - ii = np.linspace(0., limits[0], dimensions[0]) - jj = np.linspace(0., limits[1], dimensions[1]) - kk = np.linspace(0., limits[2], dimensions[2]) - - energy = np.real(0.5 * np.trapz(np.trapz(np.trapz(charge_density * potential, ii, axis=0), jj, axis=0), kk, axis=0)) * hartree_to_ev + energy = np.real(0.5 * get_integral(charge_density*potential, cell_matrix)) return orm.Float(energy) + diff --git a/aiida_defects/formation_energy/corrections/gaussian_countercharge/utils.py b/aiida_defects/formation_energy/corrections/gaussian_countercharge/utils.py index 1f9c4e1..156da9c 100644 --- a/aiida_defects/formation_energy/corrections/gaussian_countercharge/utils.py +++ b/aiida_defects/formation_energy/corrections/gaussian_countercharge/utils.py @@ -10,10 +10,32 @@ import numpy as np from aiida import orm from aiida.engine import calcfunction + """ Utility functions for the gaussian countercharge workchain """ +# def is_gaussian_isotrope(gaussian_params): +# eps = 0.01 +# average_sigma = np.mean(gaussian_params[:3]) +# #check if the off-diagonal elements sigma_xy, sigma_xz and simga_yz are all close to zero +# test_1 = all(np.array(gaussian_params[3:])/average_sigma < eps) +# test_2 = all(abs((np.array(gaussian_params[:3])/average_sigma) - 1.0) < eps) +# return test_1 and test_2 + +def is_isotrope(matrix, tol=0.01): + ''' + check if a 3x3 matrix is isotropic i,e. it can be written as a product of a number and an identity matrix + ''' + + # check if all diagonal elements are the same + diagonal = np.diagonal(matrix) + test_1 = np.all(np.abs([diagonal[0]-diagonal[1], diagonal[1]-diagonal[2], diagonal[0]-diagonal[2]]) < tol) + + #check if all the off-diagonal are zero (within the tolerance, tol) + test_2 = np.all(np.abs(matrix - np.diag(np.diagonal(matrix))) < tol) + + return test_1 and test_2 @calcfunction def create_model_structure(base_structure, scale_factor): @@ -27,38 +49,65 @@ def create_model_structure(base_structure, scale_factor): return model_structure -@calcfunction -def get_total_alignment(alignment_dft_model, alignment_q0_host, charge): - """ - Calculate the total potential alignment +# @calcfunction +# def get_total_alignment(alignment_dft_model, alignment_q0_host, charge): +# """ +# Calculate the total potential alignment - Parameters - ---------- - alignment_dft_model: orm.Float - The correction energy derived from the alignment of the DFT difference - potential and the model potential - alignment_q0_host: orm.Float - The correction energy derived from the alignment of the defect - potential in the q=0 charge state and the potential of the pristine - host structure - charge: orm.Float - The charge state of the defect +# Parameters +# ---------- +# alignment_dft_model: orm.Float +# The correction energy derived from the alignment of the DFT difference +# potential and the model potential +# alignment_q0_host: orm.Float +# The correction energy derived from the alignment of the defect +# potential in the q=0 charge state and the potential of the pristine +# host structure +# charge: orm.Float +# The charge state of the defect - Returns - ------- - total_alignment - The calculated total potential alignment +# Returns +# ------- +# total_alignment +# The calculated total potential alignment - """ +# """ + +# # total_alignment = -1.0*(charge * alignment_dft_model) + ( +# # charge * alignment_q0_host) + +# # The minus sign is incorrect. It is remove in the corrected formula below: +# total_alignment = charge * alignment_dft_model + ( +# charge * alignment_q0_host) - total_alignment = (charge * alignment_dft_model) + ( - charge * alignment_q0_host) +# return total_alignment - return total_alignment +# @calcfunction +# def get_alignment(alignment_q_host_to_model, charge): +# """ +# Calculate the total potential alignment + +# Parameters +# ---------- +# alignment_q_host_to_model: orm.Float +# The correction energy derived from the alignment of the DFT difference +# potential of the charge defect and the host to the model potential +# charge: orm.Float +# The charge state of the defect + +# Returns +# ------- +# total_alignment +# The calculated total potential alignment +# """ + +# alignment = charge.value * alignment_q_host_to_model.value + +# return orm.Float(alignment) @calcfunction -def get_total_correction(model_correction, total_alignment): +def get_total_correction(model_correction, potential_alignment, defect_charge): """ Calculate the total correction, including the potential alignments @@ -66,10 +115,9 @@ def get_total_correction(model_correction, total_alignment): ---------- model_correction: orm.Float The correction energy derived from the electrostatic model - total_alignment: orm.Float - The correction energy derived from the alignment of the DFT difference - potential and the model potential, and alignment of the defect potential - in the q=0 charge state and the potential of the pristine host structure + potential_alignment: orm.Float + The correction derived from the alignment of the difference of DFT + potential of the charge defect and the pristine host structure to the model potential Returns ------- @@ -78,7 +126,7 @@ def get_total_correction(model_correction, total_alignment): """ - total_correction = model_correction - total_alignment + total_correction = model_correction - defect_charge * potential_alignment return total_correction @@ -153,3 +201,78 @@ def calc_correction(isolated_energy, model_energy): correction_energy = isolated_energy - model_energy return orm.Float(correction_energy) + + +@calcfunction +def get_charge_model_fit(rho_host, rho_defect_q, host_structure): + """ + Fit the charge model to the defect data + + Parameters + ---------- + model_correction: orm.Float + The correction energy derived from the electrostatic model + total_alignment: orm.Float + The correction energy derived from the alignment of the DFT difference + potential and the model potential, and alignment of the defect potential + in the q=0 charge state and the potential of the pristine host structure + + Returns + ------- + total_correction + The calculated correction, including potential alignment + + """ + + from scipy.optimize import curve_fit + from .model_potential.utils import generate_charge_model, get_xyz_coords, get_cell_matrix + + # Get the cell matrix + cell_matrix = get_cell_matrix(host_structure) + + # Compute the difference in charge density between the host and defect systems + rho_defect_q_data = rho_defect_q.get_array(rho_defect_q.get_arraynames()[0]) + rho_host_data = rho_host.get_array(rho_host.get_arraynames()[0]) + + # Charge density from QE is in e/cubic-bohr, so convert if necessary + # TODO: Check if the CUBE file format is strictly Bohr or if this is a QE thing + #rho_diff = (rho_host_data - rho_defect_q_data)/(bohr_to_ang**3) + rho_diff = rho_host_data - rho_defect_q_data + + # Detect the centre of the charge in the data + max_pos_mat = np.array(np.unravel_index(rho_diff.argmax(), rho_diff.shape)) # matrix coords + max_pos_ijk = (max_pos_mat*1.)/(np.array(rho_diff.shape)-1) # Compute crystal coords + max_i = max_pos_ijk[0] + max_j = max_pos_ijk[1] + max_k = max_pos_ijk[2] + + # Generate cartesian coordinates for a grid of the same size as the charge data + xyz_coords = get_xyz_coords(cell_matrix, rho_diff.shape) + + # Set up some safe parameters for the fitting + guesses = [max_i, max_j, max_k, 1., 1., 1., 0., 0., 0.] + bounds = ( + [0., 0., 0., 0., 0., 0., 0., 0., 0.,], + [1., 1., 1., np.inf, np.inf, np.inf, np.inf, np.inf, np.inf]) + peak_charge = rho_diff.max() + + # Do the fitting + fit, covar_fit = curve_fit( + generate_charge_model(cell_matrix, peak_charge), + xyz_coords, + rho_diff.ravel(), + p0=guesses, + bounds=bounds) + + # Compute the one standard deviation errors from the 9x9 covariance array + fit_error = np.sqrt(np.diag(covar_fit)) + + fitting_results = {} + + fitting_results['fit'] = fit.tolist() + fitting_results['peak_charge'] = peak_charge + fitting_results['error'] = fit_error.tolist() + + return orm.Dict(dict=fitting_results) + + diff --git a/aiida_defects/formation_energy/defect_chemistry_base.py b/aiida_defects/formation_energy/defect_chemistry_base.py new file mode 100644 index 0000000..d2931a0 --- /dev/null +++ b/aiida_defects/formation_energy/defect_chemistry_base.py @@ -0,0 +1,455 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +from aiida import orm +from aiida.engine import WorkChain, calcfunction, ToContext, if_, submit +import numpy as np +from .chemical_potential.chemical_potential import ChemicalPotentialWorkchain + +# from .utils import ( +# get_raw_formation_energy, +# get_corrected_formation_energy, +# get_corrected_aligned_formation_energy, +# ) +from .utils import * + +class DefectChemistryWorkchainBase(WorkChain): + """ + The base class to determine the defect chemistry of a given material, containing the + generic, code-agnostic methods, error codes, etc. Defect chemistry refer to the concentration or defect formation + energy of all possible defects (vacancies, interstitials, substitutions,...) which can exist in the material at + thermodynamics equilibrium. + + Any computational code can be used to calculate the required energies and relative permittivity. + However, different codes must be setup in specific ways, and so separate classes are used to implement these + possibilities. This is an abstract class and should not be used directly, but rather the + concrete code-specific classes should be used instead. + """ + + @classmethod + def define(cls, spec): + super(DefectChemistryWorkchainBase, cls).define(spec) + + spec.input('restart_wc', valid_type=orm.Bool, required=False, default=lambda: orm.Bool(False), + help="whether to restart the workchain from previous run or to start from scratch") + spec.input('restart_node', valid_type=orm.Int, required=False, + help="if restart from previous run, provide the node corresponding to that run") + spec.input("unitcell", valid_type=orm.StructureData, + help="Pristine structure to use in the calculation of permittivity and DOS", required=False) + spec.input("host_structure", valid_type=orm.StructureData, + help="Pristine supercell without defect") + spec.input('defect_info', valid_type=orm.Dict, + help="Dictionary containing the information about defects included in the calculations of defect chemistry") + + # Chemical potential workchain + spec.expose_inputs(ChemicalPotentialWorkchain) + + # Fermi level workchain + spec.input("temperature", valid_type=orm.Float, + help="temperature at which the defect chemistry is determined. Enter in the calculaion of electron, hole and defect concentrations") + spec.input("dopant", valid_type=orm.Float, required=False, + help="Charge and concentration of aliovalent dopants added to the system. Used in the 'frozen' defect approach") + + # Input Correction workchain + spec.input("correction_scheme", valid_type=orm.Str, + help="The correction scheme to apply") + # Charge Model Settings + spec.input_namespace('charge_model', + help="Namespace for settings related to different charge models") + spec.input("charge_model.model_type", + valid_type=orm.Str, + help="Charge model type: 'fixed' or 'fitted'", + default=lambda: orm.Str('fixed')) + # Fixed + spec.input_namespace('charge_model.fixed', required=False, populate_defaults=False, + help="Inputs for a fixed charge model using a user-specified multivariate gaussian") + spec.input("charge_model.fixed.covariance_matrix", + valid_type=orm.ArrayData, + help="The covariance matrix used to construct the gaussian charge distribution.") + # Fitted + spec.input_namespace('charge_model.fitted', required=False, populate_defaults=False, + help="Inputs for a fitted charge model using a multivariate anisotropic gaussian.") + spec.input("charge_model.fitted.tolerance", + valid_type=orm.Float, + help="Permissable error for any fitted charge model parameter.", + default=lambda: orm.Float(1.0e-3)) + spec.input("charge_model.fitted.strict_fit", + valid_type=orm.Bool, + help="When true, exit the workchain if a fitting parameter is outside the specified tolerance.", + default=lambda: orm.Bool(True)) + spec.input("epsilon", valid_type=orm.ArrayData, required=False, + help="Dielectric constant of the host") + spec.input("cutoff", + valid_type=orm.Float, + default=lambda: orm.Float(100.), + help="Plane wave cutoff for electrostatic model.") + + # Outputs + spec.output( + "defect_formation_energy", valid_type=orm.Dict, required=True) + spec.output( + "chemical_potential", valid_type=orm.Dict, required=True) + spec.output( + "fermi_level", valid_type=orm.Dict, required=True) +# spec.output( +# "defect_data", valid_type=orm.Dict, required=True) + spec.output( + "total_correction", valid_type=orm.Dict, required=True) + spec.output( + "electrostatic_correction", valid_type=orm.Dict, required=True) + spec.output( + "potential_alignment", valid_type=orm.Dict, required=True) + + # Error codes + spec.exit_code(201, "ERROR_INVALID_CORRECTION", + message="The requested correction scheme is not recognised") + spec.exit_code(202, "ERROR_PARAMETER_OVERRIDE", + message="Input parameter dictionary key cannot be set explicitly") + spec.exit_code(301, "ERROR_CORRECTION_WORKCHAIN_FAILED", + message="The correction scheme sub-workchain failed") + spec.exit_code(302, "ERROR_DFT_CALCULATION_FAILED", + message="DFT calculation failed") + spec.exit_code(303, "ERROR_PP_CALCULATION_FAILED", + message="A post-processing calculation failed") + spec.exit_code(304, "ERROR_DFPT_CALCULATION_FAILED", + message="DFPT calculation failed") + spec.exit_code(305, "ERROR_DOS_CALCULATION_FAILED", + message="DOS calculation failed") + spec.exit_code(306, "ERROR_DOS_INTEGRATION_FAILED", + message="The number of electrons obtained from the integration of the DOS is different from the expected number of electrons") + spec.exit_code(401, "ERROR_CHEMICAL_POTENTIAL_WORKCHAIN_FAILED", + message="The chemical potential calculation failed") + spec.exit_code(402, "ERROR_FERMI_LEVEL_WORKCHAIN_FAILED", + message="The self-consistent fermi level calculation failed") + spec.exit_code(500, "ERROR_PARAMETER_OVERRIDE", + message="Input parameter dictionary key cannot be set explicitly") + spec.exit_code(999, "ERROR_NOT_IMPLEMENTED", + message="The requested method is not yet implemented") + + def setup(self): + """ + Setup the workchain + """ + + self.ctx.inputs_chempots = self.exposed_inputs(ChemicalPotentialWorkchain) + + # Check if correction scheme is valid: + correction_schemes_available = ["gaussian", "point"] + if self.inputs.correction_scheme is not None: + if self.inputs.correction_scheme not in correction_schemes_available: + return self.exit_codes.ERROR_INVALID_CORRECTION + + self.ctx.all_dopants = self.inputs.formation_energy_dict.get_dict() + self.ctx.chempot_dopants = self.ctx.all_dopants + self.ctx.sc_fermi_dopants = list(self.inputs.formation_energy_dict.get_dict().keys()) + #self.ctx.pw_host_dopants = list(self.inputs.formation_energy_dict.get_dict().keys()) + self.ctx.pw_host_dopants = ['intrinsic'] + + + self.ctx['output_unitcell'] = {} + #self.ctx['calc_dos'] = {} + self.ctx.dos = {} + self.ctx.all_defects = self.inputs.defect_info.get_dict() + + self.ctx.defect_data = {} + self.ctx.chemical_potential = {} + self.ctx.fermi_level = {} + self.ctx.defect_formation_energy = {} + + self.ctx.pw_defects = self.inputs.defect_info.get_dict() + self.ctx.phi_defects = self.inputs.defect_info.get_dict() + self.ctx.rho_defects = self.inputs.defect_info.get_dict() + self.ctx.gc_correction_defects = self.inputs.defect_info.get_dict() + + self.ctx.total_correction = {} + self.ctx.electrostatic_correction = {} + self.ctx.potential_alignment = {} + + # defect_data contains all the information requires to compute defect formation energy such as E_corr, E_host, vbm,... + + for defect, properties in self.ctx.all_defects.items(): + self.ctx.total_correction[defect] = {} + self.ctx.electrostatic_correction[defect] = {} + self.ctx.potential_alignment[defect] = {} + # self.ctx.defect_data[defect] = {'N_site': properties['N_site'], 'species': properties['species'], 'charges': {}} + # Add neutral defect to the calculation even if the user doesn't specify it because it is needed to generate the charge model + if 0.0 not in properties['charges']: + self.ctx.all_defects[defect]['charges'].append(0.0) + self.ctx.pw_defects[defect]['charges'].append(0.0) + self.ctx.phi_defects[defect]['charges'].append(0.0) + self.ctx.rho_defects[defect]['charges'].append(0.0) + # for chg in self.ctx.all_defects[defect]['charges']: + # self.ctx.defect_data[defect]['charges'][str(chg)] = {} + #self.report('The defect data are: {}'.format(self.ctx.defect_data)) + + + def if_restart_wc(self): + return self.inputs.restart_wc.value + + def if_rerun_calc_unitcell(self): + if not self.ctx['output_unitcell']: + return True + else: + self.ctx.number_of_electrons = self.ctx.output_unitcell['number_of_electrons'] + self.ctx.vbm = self.ctx.output_unitcell['vbm'] + self.ctx.band_gap = self.ctx.output_unitcell['band_gap'] + return False + + def if_rerun_calc_dos(self): + if not self.ctx.dos: + #self.report('start from scratch') + return True + else: + #sefl.out('density_of_states', store_dos(self.ctx.dos)) + return False + + def if_run_dfpt(self): + return self.inputs.epsilon == 0.0 + + def run_chemical_potential_workchain(self): + from .chemical_potential.chemical_potential import ( + ChemicalPotentialWorkchain, ) + + self.report('Computing the chemical potentials...') + inputs = { + "compound": self.ctx.inputs_chempots.compound, + "dependent_element": self.ctx.inputs_chempots.dependent_element, + "ref_energy": self.ctx.inputs_chempots.ref_energy, + "tolerance": self.ctx.inputs_chempots.tolerance, + } + + for key, ef_dict in self.ctx.chempot_dopants.items(): + inputs['formation_energy_dict'] = orm.Dict(dict=ef_dict) + if key != 'intrinsic': + inputs['dopant_elements'] = orm.List(list=[key]) + + workchain_future = self.submit(ChemicalPotentialWorkchain, **inputs) + workchain_future.label = key + label = "chem_potential_wc_{}".format(key) + self.to_context(**{label: workchain_future}) + # Re-initialize dopant_elements to [] + inputs['dopant_elements'] = orm.List(list=[]) + + def check_chemical_potential_workchain(self): + """ + Check if the chemical potential workchain have finished correctly. + If yes, assign the output to context + """ + + # self.ctx["chem_potential_wc_N"] = orm.load_node(230917) + # self.ctx["chem_potential_wc_intrinsic"] = orm.load_node(230921) + + for key, ef_dict in self.ctx.all_dopants.items(): + chem_potential_wc = self.ctx["chem_potential_wc_{}".format(key)] + if not chem_potential_wc.is_finished_ok: + self.report( + "Chemical potential workchain failed with status {}".format( + chem_potential_wc.exit_status + ) + ) + return self.exit_codes.ERROR_CHEMICAL_POTENTIAL_WORKCHAIN_FAILED + else: + self.ctx.chemical_potential[key] = chem_potential_wc.outputs.chemical_potential + # self.report('Chemical potentials: {}'.format(self.ctx.chemical_potential[key].get_dict())) + self.out('chemical_potential', store_dict(**self.ctx.chemical_potential)) + + def run_gaussian_correction_workchain(self): + """ + Run the workchain for the Gaussian Countercharge correction + """ + from .corrections.gaussian_countercharge.gaussian_countercharge import ( + GaussianCounterChargeWorkchain, + ) + + self.report("Computing correction via the Gaussian Countercharge scheme") + + # parent_node = orm.load_node(224010) + # self.ctx.phi_host = get_data_array(parent_node.inputs.v_host) + # self.ctx.rho_host = get_data_array(parent_node.inputs.rho_host) + # self.ctx['phi_defect_N-O[0.0]'] = get_data_array(parent_node.inputs.v_defect_q0) + # self.ctx['phi_defect_N-O[-1.0]'] = get_data_array(parent_node.inputs.v_defect_q) + # self.ctx['rho_defect_N-O[-1.0]'] = get_data_array(parent_node.inputs.rho_defect_q) + + # parent_node = orm.load_node(224014) + # self.ctx['phi_defect_V_Cl[0.0]'] = get_data_array(parent_node.inputs.v_defect_q0) + # self.ctx['phi_defect_V_Cl[1.0]'] = get_data_array(parent_node.inputs.v_defect_q) + # self.ctx['rho_defect_V_Cl[1.0]'] = get_data_array(parent_node.inputs.rho_defect_q) + + inputs = { + "v_host": self.ctx.phi_host, + "rho_host": self.ctx.rho_host, + "host_structure": self.inputs.host_structure, + "epsilon": self.ctx.epsilon, + "cutoff" : self.inputs.cutoff, + 'charge_model': { + 'model_type': self.inputs.charge_model.model_type + } + } + + #defect_info = self.ctx.all_defects + for defect, properties in self.ctx.gc_correction_defects.items(): + print(defect, properties) + inputs['defect_site'] = orm.List(list=properties['defect_position']) + inputs["v_defect_q0"] = self.ctx['phi_defect_{}[{}]'.format(defect, 0.0)] + for chg in properties['charges']: + if chg != 0.0: + inputs["defect_charge"] = orm.Float(chg) + inputs["v_defect_q"] = self.ctx['phi_defect_{}[{}]'.format(defect, chg)] + inputs["rho_defect_q"] = self.ctx['rho_defect_{}[{}]'.format(defect, chg)] + + if self.inputs.charge_model.model_type.value == 'fixed': + inputs['charge_model']['fixed'] = {'covariance_matrix': self.inputs.charge_model.fixed.covariance_matrix} + else: + inputs['charge_model']['fitted'] = {'tolerance': self.inputs.charge_model.fitted.tolerance, + 'strict_fit': self.inputs.charge_model.fitted.strict_fit} + + workchain_future = self.submit(GaussianCounterChargeWorkchain, **inputs) + label = "correction_wc_{}[{}]".format(defect, chg) + workchain_future.label = label + self.to_context(**{label: workchain_future}) + + def check_correction_workchain(self): + """ + Check if the potential alignment workchains have finished correctly. + If yes, assign the outputs to the context + """ + + # self.ctx["correction_wc_N-O[-1.0]"] = orm.load_node(231218) + # self.ctx["correction_wc_V_Cl[1.0]"] = orm.load_node(231222) + + total_correction = {} + electrostatic_correction = {} + potential_alignment = {} + for defect, properties in self.ctx.all_defects.items(): + temp_total = {} + temp_electrostatic = {} + temp_alignment ={} + for chg in properties['charges']: + # print(defect, chg) + if chg != 0.0: + correction_wc = self.ctx["correction_wc_{}[{}]".format(defect, chg)] + if not correction_wc.is_finished_ok: + self.report("Correction workchain failed with status {}" + .format(correction_wc.exit_status) + ) + return self.exit_codes.ERROR_CORRECTION_WORKCHAIN_FAILED + else: + temp_total[convert_key(str(chg))] = correction_wc.outputs.total_correction + temp_electrostatic[convert_key(str(chg))] = correction_wc.outputs.electrostatic_correction + temp_alignment[convert_key(str(chg))] = correction_wc.outputs.potential_alignment + # self.ctx.defect_data[defect]['charges'][str(chg)]['E_corr'] = correction_wc.outputs.total_correction.value + else: + temp_total[convert_key('0.0')] = orm.Float(0.0) + temp_electrostatic[convert_key('0.0')] = orm.Float(0.0) + temp_alignment[convert_key('0.0')] = orm.Float(0.0) + # self.ctx.defect_data[defect]['charges']['0.0']['E_corr'] = 0.0 + total_correction[convert_key(defect)] = store_dict(**temp_total) + electrostatic_correction[convert_key(defect)] = store_dict(**temp_electrostatic) + potential_alignment[convert_key(defect)] = store_dict(**temp_alignment) + self.ctx.total_correction = store_dict(**total_correction) + self.ctx.electrostatic_correction = store_dict(**electrostatic_correction) + self.ctx.potential_alignment = store_dict(**potential_alignment) + # self.out('defect_data', store_dict(orm.Dict(dict=self.ctx.defect_data))) + self.out('total_correction', self.ctx.total_correction) + self.out('electrostatic_correction', self.ctx.electrostatic_correction) + self.out('potential_alignment', self.ctx.potential_alignment) + # self.report('The defect data are: {}'.format(self.ctx.defect_data)) + + def create_defect_data(self): + + compound = self.inputs.compound.value + for dopant in self.ctx.sc_fermi_dopants: + pw_calc_outputs = {} + # for defect, properties in self.ctx.all_defect.items(): + for defect, properties in self.inputs.defect_info.get_dict().items(): + if is_intrinsic_defect(properties['species'], compound) or dopant in properties['species'].keys(): + for chg in properties['charges']: + pw_calc_outputs[convert_key(defect)+'_'+convert_key(str(chg))] = self.ctx['calc_defect_{}[{}]'.format(defect, chg)].outputs.output_parameters + self.ctx.defect_data[dopant] = get_defect_data(orm.Str(dopant), + self.inputs.compound, + self.inputs.defect_info, + self.ctx.vbm, + self.ctx['calc_host_intrinsic'].outputs.output_parameters, + self.ctx.total_correction, + **pw_calc_outputs) + self.report('Defect data {}: {}'.format(dopant, self.ctx.defect_data[dopant].get_dict())) + + def run_fermi_level_workchain(self): + from .fermi_level.fermi_level import ( + FermiLevelWorkchain, ) + + self.report('Running the fermi level workchain...') + + # #self.ctx.defect_data = orm.load_node(224094).get_dict() + # self.ctx.vbm = orm.load_node(224104).value + # self.ctx.number_of_electrons = orm.load_node(224105).value + # self.ctx.band_gap = orm.load_node(224106).value + # self.ctx.dos = orm.load_node(223757) + + inputs = { + "temperature": self.inputs.temperature, + "valence_band_maximum": self.ctx.vbm, + "number_of_electrons": orm.Float(self.ctx.number_of_electrons), + "unitcell": self.inputs.unitcell, + "DOS": self.ctx.dos, + "band_gap": orm.Float(self.ctx.band_gap), + #"dopant": Dict(dict={'X_1':{'c': 1E18, 'q':-1}}) + } + + for dopant in self.ctx.sc_fermi_dopants: + inputs['defect_data'] = self.ctx.defect_data[dopant] + # self.report('Defect data {}: {}'.format(dopant, defect_temp)) + inputs['chem_potentials'] = self.ctx["chem_potential_wc_{}".format(dopant)].outputs.chemical_potential + workchain_future = self.submit(FermiLevelWorkchain, **inputs) + label = "fermi_level_wc_{}".format(dopant) + workchain_future.label = dopant + self.to_context(**{label: workchain_future}) + + def check_fermi_level_workchain(self): + """ + Check if the fermi level workchain have finished correctly. + If yes, assign the output to context + """ + + #for dopant, ef_dict in self.ctx.all_dopants.items(): + for dopant in self.ctx.sc_fermi_dopants: + fermi_level_wc = self.ctx["fermi_level_wc_{}".format(dopant)] + if not fermi_level_wc.is_finished_ok: + self.report( + "Fermi level workchain of {} defect failed with status {}".format( + dopant, fermi_level_wc.exit_status)) + return self.exit_codes.ERROR_FERMI_LEVEL_WORKCHAIN_FAILED + else: + self.ctx.fermi_level[dopant] = fermi_level_wc.outputs.fermi_level + # self.ctx.fermi_level[dopant] = fermi_level_wc.outputs.fermi_level.get_array('data').item() # get the value from 0-d numpy array + # self.report('Fermi level: {}'.format(self.ctx.fermi_level[dopant].get_array('data'))) + self.out('fermi_level', store_dict(**self.ctx.fermi_level)) + + def compute_defect_formation_energy(self): + ''' + Computing the defect formation energies of all defects considered in the materials. + ''' + + #self.report('The defect data is :{}'.format(self.ctx.defect_data)) + #self.report('All dopants: {}'.format(self.ctx.all_dopants)) + #self.report('The potential alignment is :{}'.format(self.ctx.potential_alignment)) + #self.report('The chemical potentials are :{}'.format(self.ctx.chemical_potential)) + #self.report('The fermi level are :{}'.format(self.ctx.fermi_level)) + + for dopant in self.ctx.sc_fermi_dopants: + self.ctx.defect_formation_energy[dopant] = get_defect_formation_energy( + self.ctx.defect_data[dopant], + self.ctx.fermi_level[dopant], + self.ctx.chemical_potential[dopant], + self.ctx.potential_alignment, + # self.inputs.compound + ) + + # self.report('The defect formation energy is :{}'.format(self.ctx.defect_formation_energy.get_dict())) + self.out("defect_formation_energy", store_dict(**self.ctx.defect_formation_energy)) diff --git a/aiida_defects/formation_energy/defect_chemistry_qe.py b/aiida_defects/formation_energy/defect_chemistry_qe.py new file mode 100644 index 0000000..ee4f697 --- /dev/null +++ b/aiida_defects/formation_energy/defect_chemistry_qe.py @@ -0,0 +1,866 @@ +from __future__ import absolute_import + +import numpy as np + +from aiida import orm +from aiida.engine import WorkChain, calcfunction, ToContext, if_, while_, submit +from aiida.plugins import WorkflowFactory +from aiida_quantumespresso.workflows.pw.base import PwBaseWorkChain +from aiida_quantumespresso.workflows.pw.relax import PwRelaxWorkChain +from aiida_quantumespresso.workflows.protocols.utils import recursive_merge +from aiida_quantumespresso.common.types import RelaxType +from aiida.plugins import CalculationFactory, DataFactory +from aiida.orm.nodes.data.upf import get_pseudos_from_structure + +from aiida_defects.formation_energy.defect_chemistry_base import DefectChemistryWorkchainBase +from aiida_defects.formation_energy.utils import run_pw_calculation +#from .utils import get_vbm, get_raw_formation_energy, get_corrected_formation_energy, get_corrected_aligned_formation_energy +from .utils import * +import copy +import pymatgen + +PwCalculation = CalculationFactory('quantumespresso.pw') +PpCalculation = CalculationFactory('quantumespresso.pp') +PhCalculation = CalculationFactory('quantumespresso.ph') +DosCalculation = CalculationFactory('quantumespresso.dos') + +class DefectChemistryWorkchainQE(DefectChemistryWorkchainBase): + """ + Compute the formation energy for a given defect using QuantumESPRESSO + """ + @classmethod + def define(cls, spec): + super(DefectChemistryWorkchainQE, cls).define(spec) + + # DFT and DFPT calculations with QuantumESPRESSO are handled with different codes, so here + # we keep track of things with two separate namespaces. An additional code, and an additional + # namespace, is used for postprocessing + spec.input_namespace('qe.dft.supercell', + help="Inputs for DFT calculations on supercells") + spec.input_namespace('qe.dft.unitcell', + help="Inputs for a DFT calculation on an alternative host cell for use DOS and/or DFPT") + spec.input_namespace('qe.dos', + help="Inputs for DOS calculation which is needed for the Fermi level workchain") + spec.input_namespace('qe.dfpt', required=False, + help="Inputs for DFPT calculation for calculating the relative permittivity of the host material") + spec.input_namespace('qe.pp', + help="Inputs for postprocessing calculations") + + # spec.input('nbands', valid_type=orm.Int, + # help="The number of bands used in pw calculation for the unitcell. Need to specify it because we want it to be larger than the default value so that we can get the band gap which is need for the FermiLevelWorkchain.") + spec.input('k_points_distance', valid_type=orm.Float, required=False, default=lambda: orm.Float(0.2), + help='distance (in 1/Angstrom) between adjacent kpoints') + + # DFT inputs (PW.x) + spec.input("qe.dft.supercell.code", valid_type=orm.Code, + help="The pw.x code to use for the calculations") + spec.input("qe.dft.supercell.relaxation_scheme", valid_type=orm.Str, required=False, + default=lambda: orm.Str('relax'), + help="Option to relax the cell. Possible options are : ['fixed', 'relax', 'vc-relax']") + #spec.input("qe.dft.supercell.kpoints", valid_type=orm.KpointsData, + # help="The k-point grid to use for the calculations") + spec.input("qe.dft.supercell.parameters", valid_type=orm.Dict, + help="Parameters for the PWSCF calcuations. Some will be set automatically") + spec.input("qe.dft.supercell.scheduler_options", valid_type=orm.Dict, + help="Scheduler options for the PW.x calculations") + spec.input("qe.dft.supercell.settings", valid_type=orm.Dict, + help="Settings for the PW.x calculations") +# spec.input_namespace("qe.dft.supercell.pseudopotentials", valid_type=orm.UpfData, dynamic=True, +# help="The pseudopotential family for use with the code, if required") + spec.input("qe.dft.supercell.pseudopotential_family", valid_type=orm.Str, + help="The pseudopotential family for use with the code") + + # DFT inputs (PW.x) for the unitcell calculation for the dielectric constant + spec.input("qe.dft.unitcell.code", valid_type=orm.Code, + help="The pw.x code to use for the calculations") + spec.input("qe.dft.unitcell.relaxation_scheme", valid_type=orm.Str, required=False, + default=lambda: orm.Str('relax'), + help="Option to relax the cell. Possible options are : ['fixed', 'relax', 'vc-relax']") + #spec.input("qe.dft.unitcell.kpoints", valid_type=orm.KpointsData, + # help="The k-point grid to use for the calculations") + spec.input("qe.dft.unitcell.parameters", valid_type=orm.Dict, + help="Parameters for the PWSCF calcuations. Some will be set automatically") + spec.input("qe.dft.unitcell.scheduler_options", valid_type=orm.Dict, + help="Scheduler options for the PW.x calculations") + spec.input("qe.dft.unitcell.settings", valid_type=orm.Dict, + help="Settings for the PW.x calculations") +# spec.input_namespace("qe.dft.unitcell.pseudopotentials",valid_type=orm.UpfData, dynamic=True, +# help="The pseudopotential family for use with the code, if required") + spec.input("qe.dft.unitcell.pseudopotential_family", valid_type=orm.Str, + help="The pseudopotential family for use with the code") + + # DOS inputs (DOS.x) + spec.input("qe.dos.code", valid_type=orm.Code, + help="The dos.x code to use for the calculations") + spec.input("qe.dos.scheduler_options", valid_type=orm.Dict, + help="Scheduler options for the dos.x calculations") + + # Postprocessing inputs (PP.x) + spec.input("qe.pp.code", valid_type=orm.Code, + help="The pp.x code to use for the calculations") + spec.input("qe.pp.scheduler_options", valid_type=orm.Dict, + help="Scheduler options for the PP.x calculations") + + # DFPT inputs (PH.x) + spec.input("qe.dfpt.code", valid_type=orm.Code, required=False, + help="The ph.x code to use for the calculations") + spec.input("qe.dfpt.scheduler_options", valid_type=orm.Dict, required=False, + help="Scheduler options for the PH.x calculations") + + spec.outline( + cls.setup, + if_(cls.if_restart_wc)( + cls.retrieve_previous_results + ), + cls.run_chemical_potential_workchain, + cls.check_chemical_potential_workchain, + if_(cls.if_rerun_calc_unitcell)( + cls.prep_unitcell_dft_calculation, + cls.check_unitcell_dft_calculation, + ), + if_(cls.if_rerun_calc_dos)( + cls.prep_dos_calculation, + cls.check_dos_calculation, + ), + if_(cls.if_run_dfpt)( + cls.prep_dfpt_calculation + ), + cls.check_dfpt_calculation, + cls.prep_dft_calculations, + cls.check_dft_calculations, + cls.prep_dft_potentials_calculations, + cls.check_dft_potentials_calculations, + cls.prep_charge_density_calculations, + cls.check_charge_density_calculations, + cls.run_gaussian_correction_workchain, + cls.check_correction_workchain, + cls.create_defect_data, + cls.run_fermi_level_workchain, + cls.check_fermi_level_workchain, + cls.compute_defect_formation_energy + ) + + def retrieve_previous_results(self): + """ + Retrieve all the converged calculations from the previous run + """ + + self.report('Retrieving results from previous calculations...') + Node = orm.load_node(self.inputs.restart_node.value) + + # Merging and retreiving data from previous run with the that of the additional dopants + if Node.is_finished_ok: + self.ctx.chemical_potential = Node.outputs.chemical_potential.get_dict() + self.ctx.fermi_level = Node.outputs.fermi_level.get_dict() + self.ctx.total_correction = Node.outputs.total_correction.get_dict() + self.ctx.electrostatic_correction = Node.outputs.electrostatic_correction.get_dict() + self.ctx.potential_alignment = Node.outputs.potential_alignment.get_dict() + # self.ctx.defect_data = Node.outputs.defect_data.get_dict() + + self.ctx.sc_fermi_dopants = list(set(self.ctx.fermi_level.keys()).union(set(self.inputs.formation_energy_dict.get_dict().keys()))) + + for defect, properties in self.ctx.all_defects.items(): + # In case we want to add new charge states to the same defects from previous calculations + # if defect not in self.ctx.defect_data.keys(): + self.ctx.total_correction[defect] = {} + self.ctx.electrostatic_correction[defect] = {} + self.ctx.potential_alignment[defect] = {} + # self.ctx.defect_data[defect] = {'N_site': properties['N_site'], 'species': properties['species'], 'charges': {}} + if 0.0 not in properties['charges']: + self.ctx.all_defects[defect]['charges'].append(0.0) + # for chg in self.ctx.all_defects[defect]['charges']: + # self.ctx.defect_data[defect]['charges'][str(chg)] = {} + + for entry in Node.get_outgoing(): + try: + process_label = entry.node.attributes['process_label'] + #self.report('{}'.format(process_label)) + except KeyError: + continue + + #if process_label == 'PwBaseWorkChain': + # calc_label = entry.node.label + # if 'host' in calc_label: + # calc_name = 'calc_'+calc_label + # self.report('{}'.format(calc_name)) + # pw_host_dopants.remove(calc_label[5:]) + # self.ctx[calc_name] = entry.node + + if process_label == 'FermiLevelWorkchain': + self.ctx.dos = entry.node.inputs.DOS + vbm = entry.node.inputs.valence_band_maximum.value + N_electrons = entry.node.inputs.number_of_electrons.value + band_gap = entry.node.inputs.band_gap.value + self.ctx['output_unitcell'] = {'number_of_electrons': N_electrons, 'vbm': vbm, 'band_gap': band_gap} + + else: + for dopant, Ef in Node.inputs.formation_energy_dict.get_dict().items(): + if dopant not in self.ctx.all_dopants.keys(): + self.ctx.all_dopants[dopant] = Ef + chempot_dopants = copy.deepcopy(self.ctx.all_dopants) + sc_fermi_dopants = list(self.ctx.all_dopants.keys()) #copy.deepcopy(self.ctx.all_dopants) + pw_host_dopants = list(self.ctx.all_dopants.keys()) + + for defect, info in Node.inputs.defect_info.get_dict().items(): + if defect not in self.ctx.all_defects.keys(): + self.ctx.all_defects[defect] = info + if 0.0 not in self.ctx.all_defects[defect]['charges']: + self.ctx.all_defects[defect]['charges'].append(0.0) + pw_defects = copy.deepcopy(self.ctx.all_defects) + phi_defects = copy.deepcopy(self.ctx.all_defects) + rho_defects = copy.deepcopy(self.ctx.all_defects) + gc_correction_defects = copy.deepcopy(self.ctx.all_defects) + + for entry in Node.get_outgoing(): + try: + process_label = entry.node.attributes['process_label'] + #self.report('{}'.format(process_label)) + except KeyError: + continue + + if process_label == 'PwBaseWorkChain': + calc_label = entry.node.label + if calc_label == 'unitcell': + #calc_name = 'calc_unitcell' + #self.ctx['calc_unitcell'] = entry.node + vbm = get_vbm(entry.node) + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(entry.node.outputs.output_band) + N_electrons = entry.node.outputs.output_parameters.get_dict()['number_of_electrons'] + self.ctx['output_unitcell'] = {'number_of_electrons': N_electrons, 'vbm': vbm, 'band_gap': band_gap} + elif 'host' in calc_label: + calc_name = 'calc_'+calc_label + self.report('{}'.format(calc_name)) + if entry.node.is_finished_ok: + pw_host_dopants.remove(calc_label[5:]) + self.ctx[calc_name] = entry.node + else: + calc_name = 'calc_defect_'+calc_label + if entry.node.is_finished_ok: + #self.report('{}'.format(calc_name)) + self.ctx[calc_name] = entry.node + defect, chg = get_defect_and_charge_from_label(calc_label) + pw_defects[defect]['charges'].remove(chg) + if not pw_defects[defect]['charges']: + pw_defects.pop(defect) + + elif process_label == 'PpCalculation': + calc_label = entry.node.label + if entry.node.is_finished_ok: + self.ctx[calc_label] = entry.node + if 'host' not in calc_label: + defect, chg = get_defect_and_charge_from_label(calc_label[14:]) + self.report('{}, {}, {}'.format(calc_label, defect, chg)) + if 'phi' in calc_label: + # self.report('{}'.format(phi_defects)) + phi_defects[defect]['charges'].remove(chg) + if not phi_defects[defect]['charges']: + phi_defects.pop(defect) + if 'rho' in calc_label: + #self.report('{}'.format(phi_defects)) + rho_defects[defect]['charges'].remove(chg) + if not rho_defects[defect]['charges']: + rho_defects.pop(defect) + + elif process_label == 'DosCalculation': + #self.ctx['calc_dos'] = entry.node + self.ctx.dos = entry.node.outputs.output_dos + + elif process_label == 'GaussianCounterChargeWorkchain': + calc_label = entry.node.label + if entry.node.is_finished_ok: + self.ctx[calc_label] = entry.node + defect, chg = get_defect_and_charge_from_label(calc_label.replace('correction_wc_', '')) + gc_correction_defects[defect]['charges'].remove(chg) + #if not gc_correction_defects[defect]['charges']: + if gc_correction_defects[defect]['charges'] == [0.0]: + gc_correction_defects.pop(defect) + + elif process_label == 'ChemicalPotentialWorkchain': + dopant = entry.node.label + if entry.node.is_finished_ok: + self.ctx["chem_potential_wc_{}".format(dopant)] = entry.node + chempot_dopants.pop(dopant) + +# elif process_label == 'FermiLevelWorkchain': +# dopant = entry.node.label +# if entry.node.is_finished_ok: +# self.ctx["fermi_level_wc_{}".format(dopant)] = entry.node +# sc_fermi_dopants.pop(dopant) + + else: + pass + + self.ctx.chempot_dopants = chempot_dopants + self.ctx.sc_fermi_dopants = sc_fermi_dopants + self.ctx.pw_host_dopants = pw_host_dopants + self.ctx.pw_defects = pw_defects + self.ctx.phi_defects = phi_defects + self.ctx.rho_defects = rho_defects + self.ctx.gc_correction_defects = gc_correction_defects + + self.report('chempot dopant: {}'.format(self.ctx.chempot_dopants.keys())) + self.report('pw host dopant: {}'.format(self.ctx.pw_host_dopants)) + self.report('sc fermi dopants: {}'.format(self.ctx.sc_fermi_dopants)) + self.report('pw defects: {}'.format(self.ctx.pw_defects.keys())) + self.report('phi defects: {}'.format(self.ctx.phi_defects.keys())) + self.report('rho defects: {}'.format(self.ctx.rho_defects.keys())) + self.report('phi defects: {}'.format(self.ctx.phi_defects.keys())) + self.report('rho defects: {}'.format(self.ctx.rho_defects.keys())) + self.report('gc correction defects: {}'.format(self.ctx.gc_correction_defects.keys())) + + def prep_unitcell_dft_calculation(self): + """ + Run a DFT calculation on the structure to be used for the computation of the + DOS and/or dielectric constant + """ + self.report("DFT calculation for the unitcell has been requested") + + # Another code may be desirable - N.B. in AiiDA a code refers to a specific + # executable on a specific computer. As the PH calculation may have to be run on + # an HPC cluster, the PW calculation must be run on the same machine and so this + # may necessitate that a different code is used than that for the supercell calculations. + + relax_type = {'fixed': RelaxType.NONE, 'relax': RelaxType.POSITIONS, 'vc-relax': RelaxType.POSITIONS_CELL} + overrides = { + 'base':{ + # 'pseudo_family': self.inputs.qe.dft.unitcell.pseudopotential_family.value, + 'pw': {} + }, + 'base_final_scf':{ + # 'pseudo_family': self.inputs.qe.dft.unitcell.pseudopotential_family.value, + 'pw': {} + }, + 'clean_workdir' : orm.Bool(False), + } + + if 'pseudopotential_family' in self.inputs.qe.dft.unitcell: + overrides['base']['pseudo_family'] = self.inputs.qe.dft.unitcell.pseudopotential_family.value + overrides['base_final_scf']['pseudo_family'] = self.inputs.qe.dft.unitcell.pseudopotential_family.value + if 'parameters' in self.inputs.qe.dft.unitcell: + overrides['base']['pw']['parameters'] = self.inputs.qe.dft.unitcell.parameters.get_dict() + overrides['base_final_scf']['pw']['parameters'] = self.inputs.qe.dft.unitcell.parameters.get_dict() + if 'scheduler_options' in self.inputs.qe.dft.unitcell: + overrides['base']['pw']['metadata'] = self.inputs.qe.dft.unitcell.scheduler_options.get_dict() + overrides['base_final_scf']['pw']['metadata'] = self.inputs.qe.dft.unitcell.scheduler_options.get_dict() + if 'settings' in self.inputs.qe.dft.unitcell: + overrides['base']['pw']['settings'] = self.inputs.qe.dft.unitcell.settings.get_dict() + overrides['base_final_scf']['pw']['settings'] = self.inputs.qe.dft.unitcell.settings.get_dict() + + inputs = PwRelaxWorkChain.get_builder_from_protocol( + code = self.inputs.qe.dft.unitcell.code, + structure = self.inputs.unitcell, + overrides = overrides, + relax_type = relax_type[self.inputs.qe.dft.unitcell.relaxation_scheme.value] + ) + + future = self.submit(inputs) + self.report( + 'Launching PWSCF for the unitcell structure (PK={}) at node PK={}'.format(self.inputs.unitcell.pk, future.pk)) + future.label = 'unitcell' + self.to_context(**{'calc_unitcell': future}) + + def check_unitcell_dft_calculation(self): + """ + Check if the DFT calculation of the unitcell has completed successfully. + """ + + # self.ctx['calc_unitcell'] = orm.load_node(230976) + + unitcell_calc = self.ctx['calc_unitcell'] + if not unitcell_calc.is_finished_ok: + self.report( + 'PWSCF for the unitcell structure has failed with status {}'. + format(unitcell_calc.exit_status)) + return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + else: + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(unitcell_calc.outputs.output_band) + if not is_insulator: + self.report('WARNING! Metallic ground state!') + self.ctx.vbm = orm.Float(get_vbm(unitcell_calc)) + #self.ctx.number_of_electrons = unitcell_calc.res.number_of_electrons + self.ctx.number_of_electrons = unitcell_calc.outputs.output_parameters.get_dict()['number_of_electrons'] + self.ctx.band_gap = band_gap + self.report("The band gap of the material is: {} eV".format(band_gap)) + self.report("The number of electron is: {}".format(self.ctx.number_of_electrons)) + self.report("The bottom of the valence band is: {} eV".format(self.ctx.vbm.value)) + + + def prep_dos_calculation(self): + ''' + Run a calculation to extract the DOS of the unitcell. + ''' + dos_inputs = DosCalculation.get_builder() + dos_inputs.code = self.inputs.qe.dos.code + dos_inputs.parent_folder = self.ctx['calc_unitcell'].outputs.remote_folder + + parameters = orm.Dict(dict={'DOS':{ + 'Emin': -180.0, 'Emax': 40.0, 'degauss':0.0005, 'DeltaE': 0.005} + }) + dos_inputs.parameters = parameters + + dos_inputs.metadata = self.inputs.qe.dos.scheduler_options.get_dict() + + future = self.submit(DosCalculation, **dos_inputs) + self.report('Launching DOS for unitcell structure (PK={}) at node PK={}'.format(self.inputs.unitcell.pk, future.pk)) + self.to_context(**{'calc_dos': future}) + + def check_dos_calculation(self): + ''' + Retrieving the DOS of the unitcell + ''' + + # self.ctx['calc_dos'] = orm.load_node(230991) + + dos_calc = self.ctx['calc_dos'] + if dos_calc.is_finished_ok: + Dos = dos_calc.outputs.output_dos + x = Dos.get_x() + y = Dos.get_y() + DOS = np.vstack((x[1]-self.ctx.vbm.value, y[1][1])).T + mask = (DOS[:,0] <= 0.05) + N_electron = np.trapz(DOS[:,1][mask], DOS[:,0][mask]) + if np.absolute(N_electron - self.ctx.number_of_electrons) > 5e-3: + self.report('The number of electrons obtained from the integration of DOS is: {}'.format(N_electron)) + self.report('The number of electrons obtained from the integration of DOS is different from the expected number of electrons in the input') + return self.exit_codes.ERROR_DOS_INTEGRATION_FAILED + else: + self.ctx.dos = dos_calc.outputs.output_dos + else: + self.report('DOS calculation for the unitcell has failed with status {}'.format(dos_calc.exit_status)) + return self.exit_codes.ERROR_DOS_CALCULATION_FAILED + + def prep_dfpt_calculation(self): + """ + Run a DFPT calculation to compute the dielectric constant for the pristine material + """ + + ph_inputs = PhCalculation.get_builder() + ph_inputs.code = self.inputs.qe.dfpt.code + + # Setting up the calculation depends on whether the parent SCF calculation is either + # the host supercell or an alternative host unitcell + if self.inputs.unitcell: + ph_inputs.parent_folder = self.ctx['calc_unitcell'].outputs.remote_folder + else: + ph_inputs.parent_folder = self.ctx['calc_host'].outputs.remote_folder + + parameters = orm.Dict(dict={ + 'INPUTPH': { + "tr2_ph" : 1e-16, + 'epsil': True, + 'trans': False + } + }) + ph_inputs.parameters = parameters + + # Set the q-points for a Gamma-point calculation + # N.B. Setting a 1x1x1 mesh is not equivalent as this will trigger a full phonon dispersion calculation + qpoints = orm.KpointsData() + if self.inputs.host_unitcell: + qpoints.set_cell_from_structure(structuredata=self.ctx['calc_unitcell'].inputs.structure) + else: + qpoints.set_cell_from_structure(structuredata=self.ctx['calc_host'].inputs.structure) + qpoints.set_kpoints([[0.,0.,0.]]) + qpoints.get_kpoints(cartesian=True) + ph_inputs.qpoints = qpoints + + ph_inputs.metadata = self.inputs.qe.dfpt.scheduler_options.get_dict() + + future = self.submit(PhCalculation, **ph_inputs) + self.report('Launching PH for host structure at node PK={}'.format(self.inputs.host_structure.pk, future.pk)) + self.to_context(**{'calc_dfpt': future}) + + def check_dfpt_calculation(self): + """ + Compute the dielectric constant to be used in the correction + """ + if self.inputs.epsilon == 0.0: + dfpt_calc = self.ctx['calc_dfpt'] + if dfpt_calc.is_finished_ok: + epsilion_tensor = np.array(dfpt_calc.outputs.output_parameters.get_dict()['dielectric_constant']) + self.ctx.epsilon = orm.Float(np.trace(epsilion_tensor/3.)) + self.report('The computed relative permittivity is {}'.format(self.ctx.epsilon.value)) + else: + self.report( + 'PH for the host structure has failed with status {}'.format(dfpt_calc.exit_status)) + return self.exit_codes.ERROR_DFPT_CALCULATION_FAILED + else: + self.ctx.epsilon = self.inputs.epsilon + + def prep_dft_calculations(self): + """ + Run DFT calculation of the perfect host lattice as well as all the possible defects considered in the material. + """ + + relax_type = {'fixed': RelaxType.NONE, 'relax': RelaxType.POSITIONS, 'vc-relax': RelaxType.POSITIONS_CELL} + overrides = { + 'base':{ + # 'pseudo_family': self.inputs.qe.dft.unitcell.pseudopotential_family.value, + 'pw': {} + }, + 'base_final_scf':{ + # 'pseudo_family': self.inputs.qe.dft.unitcell.pseudopotential_family.value, + 'pw': {} + }, + 'clean_workdir' : orm.Bool(False), + } + + if 'pseudopotential_family' in self.inputs.qe.dft.supercell: + overrides['base']['pseudo_family'] = self.inputs.qe.dft.supercell.pseudopotential_family.value + overrides['base_final_scf']['pseudo_family'] = self.inputs.qe.dft.supercell.pseudopotential_family.value + if 'parameters' in self.inputs.qe.dft.supercell: + overrides['base']['pw']['parameters'] = self.inputs.qe.dft.supercell.parameters.get_dict() + overrides['base_final_scf']['pw']['parameters'] = self.inputs.qe.dft.supercell.parameters.get_dict() + if 'scheduler_options' in self.inputs.qe.dft.supercell: + overrides['base']['pw']['metadata'] = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + overrides['base_final_scf']['pw']['metadata'] = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + if 'settings' in self.inputs.qe.dft.supercell: + overrides['base']['pw']['settings'] = self.inputs.qe.dft.supercell.settings.get_dict() + overrides['base_final_scf']['pw']['settings'] = self.inputs.qe.dft.supercell.settings.get_dict() + + for dopant in self.ctx.pw_host_dopants: + #for dopant in self.ctx.pw_host_dopants[:1]: + #overrides['base']['pw']['metadata']['label'] = 'host_{}'.format(dopant) + inputs = PwRelaxWorkChain.get_builder_from_protocol( + code = self.inputs.qe.dft.supercell.code, + structure = self.inputs.host_structure, + overrides = overrides, + relax_type = relax_type[self.inputs.qe.dft.supercell.relaxation_scheme.value] + ) + future = self.submit(inputs) + self.report( + 'Launching PWSCF for host structure (PK={}) for {} dopant at node PK={}'.format(self.inputs.host_structure.pk, dopant, future.pk)) + future.label = 'host_{}'.format(dopant) + self.to_context(**{'calc_host_{}'.format(dopant): future}) + + + for defect, properties in self.ctx.pw_defects.items(): + defect_structure = generate_defect_structure(self.inputs.host_structure, properties['defect_position'], properties['species']) + for chg in properties['charges']: + overrides['base']['pw']['parameters'] = recursive_merge(overrides['base']['pw']['parameters'], {'SYSTEM':{'tot_charge': chg}}) + overrides['base_final_scf']['pw']['parameters'] = recursive_merge(overrides['base_final_scf']['pw']['parameters'], {'SYSTEM':{'tot_charge': chg}}) + + inputs = PwRelaxWorkChain.get_builder_from_protocol( + code = self.inputs.qe.dft.supercell.code, + structure = defect_structure, + overrides = overrides, + relax_type = relax_type[self.inputs.qe.dft.supercell.relaxation_scheme.value] + ) + + future = self.submit(inputs) + self.report('Launching PWSCF for {} defect structure with charge {} at node PK={}' + .format(defect, chg, future.pk)) + future.label = '{}[{}]'.format(defect, chg) + self.to_context(**{'calc_defect_{}[{}]'.format(defect, chg): future}) + +# def prep_dft_calculations(self): +# """ +# Run DFT calculation of the perfect host lattice as well as all the possible defects considered in the material. +# """ +# +## pw_inputs = PwCalculation.get_builder() +## pw_inputs.code = self.inputs.qe.dft.supercell.code +# +# kpoints = orm.KpointsData() +# kpoints.set_cell_from_structure(self.inputs.host_structure) +# kpoints.set_kpoints_mesh_from_density(self.inputs.k_points_distance.value) +## pw_inputs.kpoints = kpoints +# +## pw_inputs.metadata = self.inputs.qe.dft.supercell.scheduler_options.get_dict() +## pw_inputs.settings = self.inputs.qe.dft.supercell.settings +# scheduler_options = self.inputs.qe.dft.supercell.scheduler_options.get_dict() +# parameters = self.inputs.qe.dft.supercell.parameters.get_dict() +# +# # We set 'tot_charge' later so throw an error if the user tries to set it to avoid +# # any ambiguity or unseen modification of user input +# if 'tot_charge' in parameters['SYSTEM']: +# self.report('You cannot set the "tot_charge" PW.x parameter explicitly') +# return self.exit_codes.ERROR_PARAMETER_OVERRIDE +# +## pw_inputs.structure = self.inputs.host_structure +# parameters['SYSTEM']['tot_charge'] = orm.Float(0.) +## pw_inputs.parameters = orm.Dict(dict=parameters) +# +# inputs = { +# 'pw':{ +# 'code' : self.inputs.qe.dft.supercell.code, +# 'structure' : self.inputs.host_structure, +# 'parameters' : orm.Dict(dict=parameters), +# 'settings': self.inputs.qe.dft.supercell.settings, +# }, +# 'kpoints': kpoints, +# } +# +# for dopant in self.ctx.pw_host_dopants[:1]: +# pseudos = get_pseudos_from_structure(self.inputs.host_structure, self.inputs.qe.dft.supercell.pseudopotential_family.value) +# scheduler_options['label'] = 'host_{}'.format(dopant) +## pw_inputs.metadata = scheduler_options +# +# inputs['pw']['pseudos'] = pseudos +# inputs['pw']['metadata'] = scheduler_options +# +# future = self.submit(PwBaseWorkChain, **inputs) +# self.report('Launching PWSCF for host structure (PK={}) at node PK={}' +# .format(self.inputs.host_structure.pk, future.pk)) +# future.label = 'host_{}'.format(dopant) +# self.to_context(**{'calc_host_{}'.format(dopant): future}) +# +# #defect_info = self.inputs.defect_info.get_dict() +# for defect, properties in self.ctx.pw_defects.items(): +# defect_structure = generate_defect_structure(self.inputs.host_structure, properties['defect_position'], properties['species']) +## temp_structure = pymatgen.Structure.from_file('/home/sokseiham/Documents/Defect_calculations/LiK2AlF6/Structures/Ba-K.cif') +## defect_structure = orm.StructureData(pymatgen=temp_structure) +# pseudos = get_pseudos_from_structure(defect_structure, self.inputs.qe.dft.supercell.pseudopotential_family.value) +# +# inputs['pw']['structure'] = defect_structure +# inputs['pw']['pseudos'] = pseudos +# +# parameters['SYSTEM']['nspin'] = 2 +# parameters['SYSTEM']['tot_magnetization'] = 0.0 +# +# for chg in properties['charges']: +# parameters['SYSTEM']['tot_charge'] = orm.Float(chg) +## pw_inputs.parameters = orm.Dict(dict=parameters) +# scheduler_options['label'] = '{}[{}]'.format(defect, chg) +## pw_inputs.metadata = scheduler_options +# +# inputs['pw']['metadata'] = scheduler_options +# inputs['pw']['parameters'] = orm.Dict(dict=parameters) +# +# future = self.submit(PwBaseWorkChain, **inputs) +# self.report('Launching PWSCF for {} defect structure with charge {} at node PK={}' +# .format(defect, chg, future.pk)) +# future.label = '{}[{}]'.format(defect, chg) +# self.to_context(**{'calc_defect_{}[{}]'.format(defect, chg): future}) + + def check_dft_calculations(self): + """ + Check if the required calculations for the Gaussian Countercharge correction workchain + have finished correctly. + """ + + # self.ctx['calc_host_intrinsic'] = orm.load_node(231011) + # self.ctx['calc_defect_N-O[-1.0]'] = orm.load_node(231028) + # self.ctx['calc_defect_N-O[0.0]'] = orm.load_node(231044) + # self.ctx['calc_defect_V_Cl[1.0]'] = orm.load_node(231061) + # self.ctx['calc_defect_V_Cl[0.0]'] = orm.load_node(231077) + + # Host + for dopant in self.ctx.pw_host_dopants[:1]: + host_calc = self.ctx['calc_host_{}'.format(dopant)] +# if host_calc.is_finished_ok: +# self.ctx.host_energy = orm.Float(host_calc.outputs.output_parameters.get_dict()['energy']) # eV +# self.report('The energy of the host is: {} eV'.format(self.ctx.host_energy.value)) +# self.ctx.host_vbm = orm.Float(get_vbm(host_calc)) +# self.report('The top of valence band is: {} eV'.format(self.ctx.host_vbm.value)) + if not host_calc.is_finished_ok: + self.report( + 'PWSCF for the host structure has failed with status {}'.format(host_calc.exit_status)) + return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + + # Defects + #defect_info = self.inputs.defect_info.get_dict() + defect_info = self.ctx.all_defects + for defect, properties in defect_info.items(): + dopant = get_dopant(properties['species'], self.inputs.compound.value) + #self.ctx.defect_data[defect]['vbm'] = get_vbm(self.ctx['calc_host_{}'.format(dopant)]) + #self.ctx.defect_data[defect]['E_host'] = self.ctx['calc_host_{}'.format(dopant)].outputs.output_parameters.get_dict()['energy'] + # if self.ctx.pw_host_dopants == []: + # self.ctx.defect_data[defect]['vbm'] = get_vbm(self.ctx['calc_host_intrinsic']) + # self.ctx.defect_data[defect]['E_host'] = self.ctx['calc_host_intrinsic'].outputs.output_parameters.get_dict()['energy'] + # else: + # self.ctx.defect_data[defect]['vbm'] = get_vbm(self.ctx['calc_host_{}'.format(self.ctx.pw_host_dopants[0])]) + # self.ctx.defect_data[defect]['E_host'] = self.ctx['calc_host_{}'.format(self.ctx.pw_host_dopants[0])].outputs.output_parameters.get_dict()['energy'] + for chg in properties['charges']: + defect_calc = self.ctx['calc_defect_{}[{}]'.format(defect, chg)] + if not defect_calc.is_finished_ok: + self.report('PWSCF for the {} defect structure with charge {} has failed with status {}' + .format(defect, chg, defect_calc.exit_status)) + return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + else: + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(defect_calc.outputs.output_band) + if not is_insulator: + self.report('WARNING! The ground state of {} defect structure with charge {} is metallic!'.format(defect, chg)) + # self.ctx.defect_data[defect]['charges'][str(chg)]['E'] = defect_calc.outputs.output_parameters.get_dict()['energy'] # eV + self.report('The energy of {} defect structure with charge {} is: {} eV' + .format(defect, chg, defect_calc.outputs.output_parameters.get_dict()['energy'])) +# self.report('The defect data is :{}'.format(self.ctx.defect_data)) + + def prep_dft_potentials_calculations(self): + """ + Obtain the electrostatic potentials from the PWSCF calculations. + """ + # User inputs + pp_inputs = PpCalculation.get_builder() + pp_inputs.code = self.inputs.qe.pp.code + + scheduler_options = self.inputs.qe.pp.scheduler_options.get_dict() + scheduler_options['label'] = 'pp_phi_host' + pp_inputs.metadata = scheduler_options + + # Fixed settings + #pp_inputs.plot_number = orm.Int(11) # Electrostatic potential + #pp_inputs.plot_dimension = orm.Int(3) # 3D + + parameters = orm.Dict(dict={ + 'INPUTPP': { + "plot_num" : 11, + }, + 'PLOT': { + "iflag" : 3 + } + }) + pp_inputs.parameters = parameters + + # Host + # assuming that the electrostatic potential doesnt vary much with the cutoff + # pp_inputs.parent_folder = self.ctx['calc_host_intrinsic'].outputs.remote_folder + self.report('pw_host_dopants: {}'.format(self.ctx.pw_host_dopants)) + if self.ctx.pw_host_dopants == []: + pp_inputs.parent_folder = self.ctx['calc_host_intrinsic'].outputs.remote_folder + else: + pp_inputs.parent_folder = self.ctx['calc_host_{}'.format(self.ctx.pw_host_dopants[0])].outputs.remote_folder + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for electrostatic potential for the host structure at node PK={}' + .format(future.pk)) + self.to_context(**{'pp_phi_host': future}) + + #Defects + for defect, properties in self.ctx.phi_defects.items(): + for chg in properties['charges']: + scheduler_options['label'] = 'pp_phi_defect_{}[{}]'.format(defect, chg) + pp_inputs.metadata = scheduler_options + pp_inputs.parent_folder = self.ctx['calc_defect_{}[{}]'.format(defect, chg)].outputs.remote_folder + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for electrostatic potential for {} defect structure with charge {} at node PK={}' + .format(defect, chg, future.pk)) + self.to_context(**{'pp_phi_defect_{}[{}]'.format(defect, chg): future}) + + def check_dft_potentials_calculations(self): + """ + Check if the required calculations for the Gaussian Countercharge correction workchain + have finished correctly. + """ + + # self.ctx['pp_phi_host'] = orm.load_node(231144) + # self.ctx['pp_phi_defect_N-O[-1.0]'] = orm.load_node(231145) + # self.ctx['pp_phi_defect_N-O[0.0]'] = orm.load_node(231146) + # self.ctx['pp_phi_defect_V_Cl[1.0]'] = orm.load_node(231147) + # self.ctx['pp_phi_defect_V_Cl[0.0]'] = orm.load_node(231148) + + # Host + host_pp = self.ctx['pp_phi_host'] + if host_pp.is_finished_ok: + #data_array = host_pp.outputs.output_data.get_array('data') + #v_data = orm.ArrayData() + #v_data.set_array('data', data_array) + #self.ctx.phi_host = v_data + self.ctx.phi_host = get_data_array(host_pp.outputs.output_data) + else: + self.report( + 'Post processing for electrostatic potential the host structure has failed with status {}'.format(host_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED + + # Defects + defect_info = self.ctx.all_defects + for defect, properties in defect_info.items(): + for chg in properties['charges']: + defect_pp = self.ctx['pp_phi_defect_{}[{}]'.format(defect, chg)] + if defect_pp.is_finished_ok: + #data_array = defect_pp.outputs.output_data.get_array('data') + #v_data = orm.ArrayData() + #v_data.set_array('data', data_array) + #self.ctx['phi_defect_{}[{}]'.format(defect, chg)] = v_data + self.ctx['phi_defect_{}[{}]'.format(defect, chg)] = get_data_array(defect_pp.outputs.output_data) + else: + self.report('Post processing for electrostatic potential for {} defect structure with charge {} has failed with status {}' + .format(defect, chg, defect_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED + + def prep_charge_density_calculations(self): + """ + Obtain electronic charge density from the PWSCF calculations. + """ + # User inputs + pp_inputs = PpCalculation.get_builder() + pp_inputs.code = self.inputs.qe.pp.code + scheduler_options = self.inputs.qe.pp.scheduler_options.get_dict() + scheduler_options['label'] = 'pp_rho_host' + pp_inputs.metadata = scheduler_options + + # Fixed settings + #pp_inputs.plot_number = orm.Int(0) # Charge density + #pp_inputs.plot_dimension = orm.Int(3) # 3D + + parameters = orm.Dict(dict={ + 'INPUTPP': { + "plot_num" : 0, + }, + 'PLOT': { + "iflag" : 3 + } + }) + pp_inputs.parameters = parameters + + # Host + # assuming that the charge density doesn't vary much with the cutoff + pp_inputs.parent_folder = self.ctx['calc_host_{}'.format(self.ctx.pw_host_dopants[0])].outputs.remote_folder + #pp_inputs.parent_folder = self.ctx['calc_host_intrinsic'].outputs.remote_folder + + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for charge density for host structure at node PK={}' + .format(future.pk)) + self.to_context(**{'pp_rho_host': future}) + + #Defects + for defect, properties in self.ctx.rho_defects.items(): + for chg in properties['charges']: + pp_inputs.parent_folder = self.ctx['calc_defect_{}[{}]'.format(defect, chg)].outputs.remote_folder + scheduler_options['label'] = 'pp_rho_defect_{}[{}]'.format(defect, chg) + pp_inputs.metadata = scheduler_options + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for charge density for {} defect structure with charge {} at node PK={}' + .format(defect, chg, future.pk)) + self.to_context(**{'pp_rho_defect_{}[{}]'.format(defect, chg): future}) + + def check_charge_density_calculations(self): + """ + Check if the required calculations for the Gaussian Countercharge correction workchain + have finished correctly. + """ + + # self.ctx['pp_rho_host'] = orm.load_node(231180) + # self.ctx['pp_rho_defect_N-O[-1.0]'] = orm.load_node(231181) + # self.ctx['pp_rho_defect_N-O[0.0]'] = orm.load_node(231182) + # self.ctx['pp_rho_defect_V_Cl[1.0]'] = orm.load_node(231183) + # self.ctx['pp_rho_defect_V_Cl[0.0]'] = orm.load_node(231184) + + # Host + host_pp = self.ctx['pp_rho_host'] + if host_pp.is_finished_ok: + #data_array = host_pp.outputs.output_data.get_array('data') + #v_data = orm.ArrayData() + #v_data.set_array('data', data_array) + #self.ctx.rho_host = v_data + self.ctx.rho_host = get_data_array(host_pp.outputs.output_data) + else: + self.report( + 'Post processing for charge density for the host structure has failed with status {}'.format(host_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED + + # Defects + defect_info = self.ctx.all_defects + for defect, properties in defect_info.items(): + for chg in properties['charges']: + defect_pp = self.ctx['pp_rho_defect_{}[{}]'.format(defect, chg)] + if defect_pp.is_finished_ok: + #data_array = defect_pp.outputs.output_data.get_array('data') + #v_data = orm.ArrayData() + #v_data.set_array('data', data_array) + #self.ctx['rho_defect_{}[{}]'.format(defect, chg)] = v_data + self.ctx['rho_defect_{}[{}]'.format(defect, chg)] = get_data_array(defect_pp.outputs.output_data) + else: + self.report('Post processing for charge density for {} defect structure with charge {} has failed with status {}' + .format(defect, chg, defect_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED + diff --git a/aiida_defects/formation_energy/fermi_level/__init__.py b/aiida_defects/formation_energy/fermi_level/__init__.py new file mode 100644 index 0000000..4d27567 --- /dev/null +++ b/aiida_defects/formation_energy/fermi_level/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## diff --git a/aiida_defects/formation_energy/fermi_level/fermi_level.py b/aiida_defects/formation_energy/fermi_level/fermi_level.py new file mode 100644 index 0000000..2fa2c87 --- /dev/null +++ b/aiida_defects/formation_energy/fermi_level/fermi_level.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +from aiida.orm import Float, Int, Str, List, Bool, Dict, ArrayData, XyData, StructureData +from aiida.engine import WorkChain, calcfunction, ToContext, while_ +import sys +import numpy as np +from scipy.optimize.nonlin import NoConvergence +from pymatgen.core.composition import Composition + +from .utils import * + +class FermiLevelWorkchain(WorkChain): + ''' + Compute the self-consistent Fermi level by imposing the overall charge neutrality + Here we implement method similar to Buckeridge et al., (doi:10.1016/j.cpc.2019.06.017) + ''' + @classmethod + def define(cls, spec): + super(FermiLevelWorkchain, cls).define(spec) + spec.input("defect_data", valid_type=Dict) + spec.input("chem_potentials", valid_type=Dict) + spec.input("temperature", valid_type=Float) + spec.input("valence_band_maximum", valid_type=Float) + spec.input("number_of_electrons", valid_type=Float, help="number of electrons in the unitcell used to compute the DOS") + spec.input("unitcell", valid_type=StructureData) + spec.input("DOS", valid_type=XyData) + spec.input("band_gap", valid_type=Float) + spec.input("dopant", valid_type=Dict, default=lambda: Dict(dict=None), + help="aliovalent dopants specified by its charge and concentration. Used to compute the change in the defect concentrations with frozen defect approach") + spec.input("tolerance_factor", valid_type=Float, default=lambda: Float(1e-10), + help="tolerance factor use in the non-linear solver to solve for the self-consistent fermi level") + + spec.outline( + cls.setup, + cls.compute_sc_fermi_level, + ) + spec.output('fermi_level', valid_type=ArrayData) # we use ArrayData instead of Float in other to be general and be able to accomodate the situtation where the chemical potential is a numpy array allowing to vectorize the calculations of defect concentrations in stability region instead of doing one value of chemical potential at a time. + + spec.exit_code(701, "ERROR_FERMI_LEVEL_FAILED", + message="The number of electrons obtained from the integration of DOS is different from the expected number of electrons in the input" + ) + spec.exit_code(702, "ERROR_NON_LINEAR_SOLVER_FAILED", + message="The non-linear solver used to solve for the self-consistent Fermi level failed. The tolerance factor might be too small" + ) + + def setup(self): + """ + Setup the calculation + """ + chempot_dict = self.inputs.chem_potentials.get_dict() +# for key, value in chempot_dict.items(): +# data_array = np.ones_like(value) +# #print(data_array) +# v_data = ArrayData() +# v_data.set_array('data', data_array) +# self.ctx.input_chem_shape = v_data + + # extracting the DOS of the unitcell, assuming that the calculation is non-spin polarized. + dos_x = self.inputs.DOS.get_x()[1] - self.inputs.valence_band_maximum.value # Shift the top of valence band to zero + v_data = ArrayData() + v_data.set_array('data', dos_x) + self.ctx.dos_x = v_data + + dos_y = self.inputs.DOS.get_y()[1][1] + v_data = ArrayData() + v_data.set_array('data', dos_y) + self.ctx.dos_y = v_data + + mask = (dos_x <= 0.05) + N_electron = np.trapz(dos_y[mask], dos_x[mask]) + if np.absolute(N_electron-self.inputs.number_of_electrons.value) > 5e-3: + self.report('The number of electrons obtained from the integration of DOS is: {}'.format(N_electron)) + self.report('The number of electrons obtained from the integration of DOS is different from the expected number of electrons in the input') + return self.exit_codes.ERROR_FERMI_LEVEL_FAILED + + #is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(unitcell_node.outputs.output_band) + #if not is_insulator: + #self.report('WARNING!') + #self.report('The compound is metallic!') + + def compute_sc_fermi_level(self): + try: + E_Fermi = solve_for_sc_fermi(self.inputs.defect_data, + self.inputs.chem_potentials, + #self.ctx.input_chem_shape, + self.inputs.temperature, + self.inputs.unitcell, + self.inputs.band_gap, + self.ctx.dos_x, + self.ctx.dos_y, + self.inputs.dopant, + self.inputs.tolerance_factor) + + self.ctx.sc_fermi_level = E_Fermi + self.out('fermi_level', E_Fermi) + self.report('The self-consistent Fermi level is: {} eV'.format(E_Fermi.get_array('data'))) + except NoConvergence: + self.report("The non-linear solver used to solve for the self-consistent Fermi level failed. The tolerance factor might be too small") + return self.exit_codes.ERROR_NON_LINEAR_SOLVER_FAILED diff --git a/aiida_defects/formation_energy/fermi_level/utils.py b/aiida_defects/formation_energy/fermi_level/utils.py new file mode 100644 index 0000000..4afe035 --- /dev/null +++ b/aiida_defects/formation_energy/fermi_level/utils.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +from aiida.engine import calcfunction +import numpy as np +import random +from pymatgen.core.composition import Composition +from aiida.orm import ArrayData, Float +from scipy.optimize import broyden1 +from scipy.optimize.nonlin import NoConvergence +from scipy.special import expit + + +def compute_net_charge(defect_data, chem_potentials, temperature, unitcell, band_gap, dos_x, dos_y, dopant): + ''' + This is a nested function that return a function (with E_Fermi as variable) to be use in the + non-linear solver to obtain the self-consistent Fermi level. + + arguments: + defect_data : dictionary containing information required to compute the formation energy of each defect + chem_potentials : dictionary containing the chemical potential of all elements constituting the compound. Each value can be a float or 1d numpy array + #input_chem_shape : the shape of values of 'chem_potentials' dictionnary. This is needed because we want the code to work both for float or numpy array + # for ex. when computing the concentration of a particular defect in the stability region. We can of course do that one + # value at a time but it is much slower than vectorization using numpy + dopant : aliovalent dopants specified by its charge and concentration with the format {'X_1': {'c':, 'q':}, 'X_2': {'c':, 'q':}, ...}. + Used to compute the change in the defect concentrations with 'frozen defect' approach + uniticell : is the structure used to compute the Dos, NOT the host supercell used to compute the formation energy + ''' + + dE = dos_x[1] - dos_x[0] + k_B = 8.617333262145E-05 + convert = 1E24 + + def defect_formation_energy(E_Fermi): + ''' + Compute the defect formation energy of all defects given in the input file as a function of the fermi level + E_Fermi. + ''' + E_defect_formation = {} + for defect in defect_data.keys(): + temp = defect_data[defect] + Ef = {} + for chg in temp['charges'].keys(): + E_formation = temp['charges'][chg]['E']-temp['E_host']+float(chg)*(E_Fermi+temp['vbm'])+temp['charges'][chg]['E_corr'] + for spc in temp['species'].keys(): + E_formation -= temp['species'][spc]*np.array(chem_potentials[spc]) # We convert chem_potentials[spc] to np array because it was converted to list in the calcfunction + Ef[chg] = E_formation + E_defect_formation[defect] = Ef + return E_defect_formation + + def electron_concentration(E_Fermi): + ''' + Compute electron concentration + ''' +# if input_chem_shape.ndim != 0: + upper_dos = np.reshape(dos_y[dos_x>=band_gap], (-1,1)) # For broadcasting + E_upper = np.reshape(dos_x[dos_x>=band_gap], (-1,1)) # For broadcasting +# else: +# upper_dos = dos_y[dos_x>=band_gap] +# E_upper = dos_x[dos_x>=band_gap] + temp_n = upper_dos*expit(-(E_upper-E_Fermi)/(k_B*temperature)) # Use expit (expit(x)=1/(1+exp(-x))) to directly compute Fermi-Dirac distribution + return convert*np.sum(temp_n, axis=0)*dE/unitcell.volume + + def hole_concentration(E_Fermi): + ''' + Compute hole concentration + ''' +# if input_chem_shape.ndim != 0: + lower_dos = np.reshape(dos_y[dos_x<=0.0], (-1,1)) #For broadcasting + E_lower = np.reshape(dos_x[dos_x<=0.0], (-1,1)) #For broadcasting +# else: +# lower_dos = dos_y[dos_x<=0.0] +# E_lower = dos_x[dos_x<=0.0] + temp_p = lower_dos*expit(-(E_Fermi-E_lower)/(k_B*temperature)) # Use expit to directly compute Fermi-Dirac distribution + return convert*np.sum(temp_p, axis=0)*dE/unitcell.volume + +# def electron_concentration(E_Fermi): +# ''' +# Compute electron concentration +# ''' +# upper_dos = dos_y[dos_x>=band_gap] +# E_upper = dos_x[dos_x>=band_gap] +# +# ndim = E_Fermi.ndim +# E_Fermi = np.expand_dims(E_Fermi, axis=ndim) # To broadcast with E_upper +# mask_n = ((E_upper-E_Fermi)/(k_B*temperature) < 700.0) # To avoid overflow in the exp +# for i in range(ndim): +# upper_dos = np.repeat(np.expand_dims(upper_dos, axis=0), E_Fermi.shape[ndim-i-1], axis=0) +# upper_dos[~mask_n] = 0 +# temp = E_upper-E_Fermi +# temp[~mask_n] = 0 +# temp_n = upper_dos/(np.exp(temp/(k_B*temperature))+1.0) +# +# return convert*np.sum(temp_n, axis=ndim)*dE/unitcell.volume +# +# def hole_concentration(E_Fermi): +# ''' +# Compute hole concentration +# ''' +# lower_dos = dos_y[dos_x<=0.0] +# E_lower = dos_x[dos_x<=0.0] +# +# ndim = E_Fermi.ndim +# E_Fermi = np.expand_dims(E_Fermi, axis=ndim) # To broadcast with E_lower +# mask_p = ((E_Fermi-E_lower)/(k_B*temperature) < 700.0) # To avoid overflow in the exp +# for i in range(ndim): +# lower_dos = np.repeat(np.expand_dims(lower_dos, axis=0), E_Fermi.shape[ndim-i-1], axis=0) +# lower_dos[~mask_p] = 0 +# temp = E_Fermi-E_lower +# temp[~mask_p] = 0 +# temp_p = lower_dos/(np.exp(temp/(k_B*temperature))+1.0) +# +# return convert*np.sum(temp_p, axis=ndim)*dE/unitcell.volume + + def c_defect(N_site, Ef): + ''' + compute the concentration of defects having formation energy Ef and can exist in N_sites in the unitcell + ''' + return convert*N_site*np.exp(-1.0*Ef/(k_B*temperature))/unitcell.volume + + def Net_charge(E_Fermi): + ''' + compute the total charge of the system. The self-consistent Fermi level is the one for which this net (or total) charge is zero. + ''' + n = electron_concentration(E_Fermi) + p = hole_concentration(E_Fermi) + E_defect_formation = defect_formation_energy(E_Fermi) + # print(n, p) + # positive_charge = np.zeros(4) + # negative_charge = np.zeros(4) + positive_charge = 0.0 + negative_charge = 0.0 + for key in E_defect_formation.keys(): + for chg in E_defect_formation[key]: + # print(chg) + if float(chg) > 0: + positive_charge += float(chg)*c_defect(defect_data[key]['N_site'], E_defect_formation[key][chg]) + else: + negative_charge += float(chg)*c_defect(defect_data[key]['N_site'], E_defect_formation[key][chg]) + if dopant != None: + for key in dopant.keys(): + if dopant[key]['q'] > 0: + positive_charge += dopant[key]['q']*dopant[key]['c'] + else: + negative_charge += dopant[key]['q']*dopant[key]['c'] + return np.log(p + positive_charge) - np.log(n + abs(negative_charge)) + + return Net_charge + +@calcfunction +def solve_for_sc_fermi(defect_data, chem_potentials, temperature, unitcell, band_gap, dos_x, dos_y, dopant, f_tol): + ''' + solve the non-linear equation with E_fermi as variable to obtain the self-consistent Fermi level. The non-linear solver broyden1 in + scipy is used. + ''' + + defect_data = defect_data.get_dict() + chem_potentials = chem_potentials.get_dict() + #input_chem_shape = input_chem_shape.get_array('data') + temperature = temperature.value + unitcell = unitcell.get_pymatgen_structure() + band_gap = band_gap.value + dos_x = dos_x.get_array('data') + dos_y = dos_y.get_array('data') + dopant = dopant.get_dict() + tolerance = f_tol.value + + net_charge = compute_net_charge(defect_data, chem_potentials, temperature, unitcell, band_gap, dos_x, dos_y, dopant) + #sc_fermi = broyden1(net_charge, input_chem_shape*band_gap/2, f_tol=tolerance) + input_chem_shape = np.ones_like(random.choice(list(chem_potentials.values()))) + sc_fermi = broyden1(net_charge, input_chem_shape*band_gap/2, f_tol=tolerance) + v_data = ArrayData() + v_data.set_array('data', sc_fermi) + return v_data diff --git a/aiida_defects/formation_energy/formation_energy_base.py b/aiida_defects/formation_energy/formation_energy_base.py index 5f71dec..8b4f28a 100644 --- a/aiida_defects/formation_energy/formation_energy_base.py +++ b/aiida_defects/formation_energy/formation_energy_base.py @@ -10,37 +10,39 @@ from aiida import orm from aiida.engine import WorkChain, calcfunction, ToContext, if_, submit +from .corrections.gaussian_countercharge.gaussian_countercharge import ( + GaussianCounterChargeWorkchain) from .utils import ( get_raw_formation_energy, get_corrected_formation_energy, - get_corrected_aligned_formation_energy, -) + get_corrected_aligned_formation_energy) + class FormationEnergyWorkchainBase(WorkChain): """ - The base class to compute the formation energy for a given defect, containing the + The base class to compute the formation energy for a given defect, containing the generic, code-agnostic methods, error codes, etc. - Any computational code can be used to calculate the required energies and relative permittivity. - However, different codes must be setup in specific ways, and so separate classes are used to implement these - possibilities. This is an abstract class and should not be used directly, but rather the - concrete code-specific classes should be used instead. + Any computational code can be used to calculate the required energies and relative permittivity. + However, different codes must be setup in specific ways, and so separate classes are used to implement these + possibilities. This is an abstract class and should not be used directly, but rather the + concrete code-specific classes should be used instead. """ @classmethod def define(cls, spec): super(FormationEnergyWorkchainBase, cls).define(spec) # fmt: off - # Structures + # Structures spec.input( - "host_structure", - valid_type=orm.StructureData, + "host_structure", + valid_type=orm.StructureData, help="Pristine structure" ) spec.input( - "defect_structure", - valid_type=orm.StructureData, + "defect_structure", + valid_type=orm.StructureData, help="Defective structure" ) spec.input( @@ -52,70 +54,113 @@ def define(cls, spec): # Defect details spec.input( - "defect_charge", - valid_type=orm.Float, - help="Defect charge state") + "defect_charge", + valid_type=orm.Float, + help="Defect charge state") +# spec.input( +# "defect_species", +# valid_type=orm.Str) spec.input( "defect_site", valid_type=orm.List, - help="Defect site position in crystal coordinates", - ) + help="Defect site position in crystal coordinates" ) spec.input( - "fermi_level", - valid_type=orm.Float, - default=orm.Float(0.0), - help="Fermi level position with respect to the valence band maximum", - ) + "fermi_level", + valid_type=orm.Float, + default=lambda: orm.Float(0.0), + help="Fermi level position with respect to the valence band maximum") + spec.input("chempot_sign", + valid_type=orm.Dict, + help="To determine the sign of the chemical potential. The convention is that removing an atom is negative") + + # Chemical potential + spec.input('run_chem_pot_wc', valid_type=orm.Bool, default=lambda: orm.Bool(True)) + spec.input('formation_energy_dict', required=False, valid_type=orm.Dict) + spec.input('compound', required=False, valid_type=orm.Str) + spec.input('dependent_element', required=False, valid_type=orm.Str) + spec.input("dopant_elements", valid_type=orm.List, default=lambda: orm.List(list=[])) + spec.input("ref_energy", valid_type=orm.Dict, required=False, help="The reference chemical potential of elements in the structure") + spec.input('tolerance', valid_type=orm.Float, default=lambda: orm.Float(1E-4)) spec.input( - "chemical_potential", + "chemical_potential", + valid_type=orm.Dict, required=False, + help="The chemical potential of the given defect type. The convention is that removing an atom is positive") + + # Input for correction workchain + # Charge Model Settings + spec.input_namespace('charge_model', + help="Namespace for settings related to different charge models") + spec.input("charge_model.model_type", + valid_type=orm.Str, + help="Charge model type: 'fixed' or 'fitted'", + default=lambda: orm.Str('fixed')) + # Fixed + spec.input_namespace('charge_model.fixed', required=False, populate_defaults=False, + help="Inputs for a fixed charge model using a user-specified multivariate gaussian") + spec.input("charge_model.fixed.covariance_matrix", + valid_type=orm.ArrayData, + help="The covariance matrix used to construct the gaussian charge distribution.") + # "gaussian charge distribution. The format required is " + # "[x0, y0, z0, sigma_x, sigma_y, sigma_z, cov_xy, cov_xz, cov_yz]") + # Fitted + spec.input_namespace('charge_model.fitted', required=False, populate_defaults=False, + help="Inputs for a fitted charge model using a multivariate anisotropic gaussian.") + spec.input("charge_model.fitted.tolerance", valid_type=orm.Float, - help="The chemical potential of the given defect type. The convention is that removing an atom is positive", - ) + help="Permissable error for any fitted charge model parameter.", + default=lambda: orm.Float(1.0e-3)) + spec.input("charge_model.fitted.strict_fit", + valid_type=orm.Bool, + help="When true, exit the workchain if a fitting parameter is outside the specified tolerance.", + default=lambda: orm.Bool(True)) +# spec.input('sigma', valid_type=orm.Float, required=False) + spec.input("epsilon", valid_type=orm.ArrayData, help="3x3 dielectric tensor of the host", required=True) + spec.input("cutoff", valid_type=orm.Float, required=False) + + spec.input("run_dfpt", valid_type=orm.Bool) # Methodology spec.input( "correction_scheme", valid_type=orm.Str, - help="The correction scheme to apply", - ) + help="The correction scheme to apply") + # Optional parameters to override the gaussian charge model settings + spec.expose_inputs(GaussianCounterChargeWorkchain, + namespace='gaussian', + include=['charge_model']) + # Outputs spec.output( - "formation_energy_uncorrected", valid_type=orm.Float, required=True - ) + "formation_energy_uncorrected", valid_type=orm.Float, required=True) spec.output( - "formation_energy_corrected", valid_type=orm.Float, required=True - ) + "formation_energy_corrected", valid_type=orm.Float, required=True) spec.output( - "formation_energy_corrected_aligned", valid_type=orm.Float, required=True - ) + "formation_energy_corrected_aligned", valid_type=orm.Float, required=True) # Error codes - spec.exit_code( 401, "ERROR_INVALID_CORRECTION", - message="The requested correction scheme is not recognised", - ) - spec.exit_code(402, "ERROR_CORRECTION_WORKCHAIN_FAILED", - message="The correction scheme sub-workchain failed", - ) - spec.exit_code(403, "ERROR_DFT_CALCULATION_FAILED", - message="DFT calculation failed", - ) - spec.exit_code(404, "ERROR_PP_CALCULATION_FAILED", - message="A post-processing calculation failed", - ) - spec.exit_code(405, "ERROR_DFPT_CALCULATION_FAILED", - message="DFPT calculation failed" - ) + spec.exit_code(201, "ERROR_INVALID_CORRECTION", + message="The requested correction scheme is not recognised",) + spec.exit_code(202, "ERROR_PARAMETER_OVERRIDE", + message="Input parameter dictionary key cannot be set explicitly",) + spec.exit_code(301, "ERROR_CORRECTION_WORKCHAIN_FAILED", + message="The correction scheme sub-workchain failed",) + spec.exit_code(302, "ERROR_DFT_CALCULATION_FAILED", + message="DFT calculation failed",) + spec.exit_code(303, "ERROR_PP_CALCULATION_FAILED", + message="A post-processing calculation failed",) + spec.exit_code(304, "ERROR_DFPT_CALCULATION_FAILED", + message="DFPT calculation failed") + spec.exit_code(406, "ERROR_CHEMICAL_POTENTIAL_WORKCHAIN_FAILED", + message="The chemical potential calculation failed") spec.exit_code(500, "ERROR_PARAMETER_OVERRIDE", - message="Input parameter dictionary key cannot be set explicitly", - ) + message="Input parameter dictionary key cannot be set explicitly") spec.exit_code(999, "ERROR_NOT_IMPLEMENTED", - message="The requested method is not yet implemented", - ) + message="The requested method is not yet implemented") # fmt: on def setup(self): - """ + """ Setup the workchain """ @@ -125,6 +170,12 @@ def setup(self): if self.inputs.correction_scheme not in correction_schemes_available: return self.exit_codes.ERROR_INVALID_CORRECTION + def if_run_dfpt(self): + return self.inputs.run_dfpt + + def if_run_chem_pot_wc(self): + return self.inputs.run_chem_pot_wc + def correction_required(self): """ Check if correction is requested @@ -160,21 +211,38 @@ def run_gaussian_correction_workchain(self): """ Run the workchain for the Gaussian Countercharge correction """ - from .corrections.gaussian_countercharge.gaussian_countercharge import ( - GaussianCounterChargeWorkchain, - ) self.report("Computing correction via the Gaussian Countercharge scheme") + if self.inputs.gaussian.charge_model: + charge_model_dict = self.inputs.gaussian.charge_model + else: + charge_model_dict = { + 'model_type': Str('fitted'), + 'fitted': {} + } + inputs = { "v_host": self.ctx.v_host, "v_defect_q0": self.ctx.v_defect_q0, "v_defect_q": self.ctx.v_defect_q, + "rho_host": self.ctx.rho_host, + "rho_defect_q": self.ctx.rho_defect_q, "defect_charge": self.inputs.defect_charge, "defect_site": self.inputs.defect_site, "host_structure": self.inputs.host_structure, "epsilon": self.ctx.epsilon, + "cutoff" : self.inputs.cutoff, + 'charge_model': { + 'model_type': self.inputs.charge_model.model_type + } + } + if self.inputs.charge_model.model_type.value == 'fixed': + inputs['charge_model']['fixed'] = {'covariance_matrix': self.inputs.charge_model.fixed.covariance_matrix} + else: + inputs['charge_model']['fitted'] = {'tolerance': self.inputs.charge_model.fitted.tolerance, + 'strict_fit': self.inputs.charge_model.fitted.strict_fit} workchain_future = self.submit(GaussianCounterChargeWorkchain, **inputs) label = "correction_workchain" @@ -219,28 +287,66 @@ def check_correction_workchain(self): correction_wc.exit_status ) ) - return self.exit_codes.ERROR_SUB_PROCESS_FAILED_CORRECTION + return self.exit_codes.ERROR_CORRECTION_WORKCHAIN_FAILED else: self.ctx.total_correction = correction_wc.outputs.total_correction self.ctx.electrostatic_correction = ( correction_wc.outputs.electrostatic_correction ) - self.ctx.total_alignment = correction_wc.outputs.total_alignment + self.ctx.potential_alignment = correction_wc.outputs.potential_alignment + + def run_chemical_potential_workchain(self): + from .chemical_potential.chemical_potential import ( + ChemicalPotentialWorkchain, ) + + self.report('Submitting the chemical potential workchain') + inputs = { + "formation_energy_dict": self.inputs.formation_energy_dict, + "compound": self.inputs.compound, + "dependent_element": self.inputs.dependent_element, + "dopant_elements": self.inputs.dopant_elements, + "ref_energy": self.inputs.ref_energy, + "tolerance": self.inputs.tolerance, + } + workchain_future = self.submit(ChemicalPotentialWorkchain, **inputs) + label = "chemical_potential_workchain" + self.to_context(**{label: workchain_future}) + def check_chemical_potential_workchain(self): + """ + Check if the chemical potential workchain have finished correctly. + If yes, assign the output to context + """ + + if self.inputs.run_chem_pot_wc: + chem_potential_wc = self.ctx["chemical_potential_workchain"] + if not chem_potential_wc.is_finished_ok: + self.report( + "Chemical potential workchain failed with status {}".format( + chem_potential_wc.exit_status + ) + ) + return self.exit_codes.ERROR_CHEMICAL_POTENTIAL_WORKCHAIN_FAILED + #return self.exit_codes.ERROR_SUB_PROCESS_FAILED_CORRECTION + else: + self.ctx.chemical_potential = chem_potential_wc.outputs.chemical_potential + else: + self.ctx.chemical_potential = self.inputs.chemical_potential + def compute_formation_energy(self): - """ + """ Compute the formation energy """ - # Raw formation energy self.ctx.e_f_uncorrected = get_raw_formation_energy( self.ctx.defect_energy, self.ctx.host_energy, - self.inputs.chemical_potential, + self.inputs.chempot_sign, + self.ctx.chemical_potential, self.inputs.defect_charge, self.inputs.fermi_level, - self.ctx.host_vbm, - ) + self.ctx.host_vbm + ) self.report( "The computed uncorrected formation energy is {} eV".format( self.ctx.e_f_uncorrected.value @@ -261,7 +367,7 @@ def compute_formation_energy(self): # Corrected formation energy with potential alignment self.ctx.e_f_corrected_aligned = get_corrected_aligned_formation_energy( - self.ctx.e_f_corrected, self.ctx.total_alignment + self.ctx.e_f_corrected, self.inputs.defect_charge, self.ctx.potential_alignment ) self.report( "The computed corrected formation energy, including potential alignments, is {} eV".format( diff --git a/aiida_defects/formation_energy/formation_energy_qe.py b/aiida_defects/formation_energy/formation_energy_qe.py index e1785f2..0335c6c 100644 --- a/aiida_defects/formation_energy/formation_energy_qe.py +++ b/aiida_defects/formation_energy/formation_energy_qe.py @@ -11,13 +11,17 @@ from aiida import orm from aiida.engine import WorkChain, calcfunction, ToContext, if_, submit -from aiida.plugins import WorkflowFactory +from aiida.plugins import CalculationFactory, WorkflowFactory from aiida_quantumespresso.workflows.pw.base import PwBaseWorkChain +from aiida_quantumespresso.workflows.pw.relax import PwRelaxWorkChain +from aiida_quantumespresso.workflows.protocols.utils import recursive_merge +from aiida_quantumespresso.common.types import RelaxType from aiida_defects.formation_energy.formation_energy_base import FormationEnergyWorkchainBase from aiida_defects.formation_energy.utils import run_pw_calculation -from .utils import get_raw_formation_energy, get_corrected_formation_energy, get_corrected_aligned_formation_energy +from .utils import get_vbm, get_raw_formation_energy, get_data_array, get_corrected_formation_energy, get_corrected_aligned_formation_energy +PpCalculation = CalculationFactory('quantumespresso.pp') class FormationEnergyWorkchainQE(FormationEnergyWorkchainBase): """ @@ -40,43 +44,57 @@ def define(cls, spec): help="Inputs for postprocessing calculations") + # What calculations to run + spec.input('run_pw_host', valid_type=orm.Bool, required=True) # TODO: Check why these are here - for restarts? + spec.input('run_pw_defect_q0', valid_type=orm.Bool, required=True) + spec.input('run_pw_defect_q', valid_type=orm.Bool, required=True) + spec.input('run_v_host', valid_type=orm.Bool, required=True) + spec.input('run_v_defect_q0', valid_type=orm.Bool, required=True) + spec.input('run_v_defect_q', valid_type=orm.Bool, required=True) + spec.input('run_rho_host', valid_type=orm.Bool, required=True) + spec.input('run_rho_defect_q0', valid_type=orm.Bool, required=True) + spec.input('run_rho_defect_q', valid_type=orm.Bool, required=True) + spec.input('run_dfpt', valid_type=orm.Bool, required=True) + + spec.input('host_node', valid_type=orm.Int, required=False) # TODO: Need to look at this if this is intended for passing parent calcs + spec.input('defect_q0_node', valid_type=orm.Int, required=False) + spec.input('defect_q_node', valid_type=orm.Int, required=False) + spec.input('v_host_node', valid_type=orm.Int, required=False) + spec.input('v_defect_q0_node', valid_type=orm.Int, required=False) + spec.input('v_defect_q_node', valid_type=orm.Int, required=False) + spec.input('rho_host_node', valid_type=orm.Int, required=False) + spec.input('rho_defect_q0_node', valid_type=orm.Int, required=False) + spec.input('rho_defect_q_node', valid_type=orm.Int, required=False) + spec.input("relaxation_scheme", valid_type=orm.Str, required=False, + default=lambda: orm.Str('vc-relax'), + help="Option to relax the cell. Possible options are : ['fixed', 'relax', 'vc-relax']") + # DFT inputs (PW.x) - spec.input("qe.dft.supercell.code", - valid_type=orm.Code, + spec.input("qe.dft.supercell.code", valid_type=orm.Code, help="The pw.x code to use for the calculations") - spec.input("qe.dft.supercell.kpoints", - valid_type=orm.KpointsData, - help="The k-point grid to use for the calculations") - spec.input("qe.dft.supercell.parameters", - valid_type=orm.Dict, + spec.input("qe.dft.supercell.parameters", valid_type=orm.Dict, required=False, help="Parameters for the PWSCF calcuations. Some will be set automatically") - spec.input("qe.dft.supercell.scheduler_options", - valid_type=orm.Dict, + spec.input("qe.dft.supercell.scheduler_options", valid_type=orm.Dict, help="Scheduler options for the PW.x calculations") - spec.input_namespace("qe.dft.supercell.pseudopotentials", - valid_type=orm.UpfData, - dynamic=True, - help="The pseudopotential family for use with the code, if required" - ) + spec.input("qe.dft.supercell.settings", valid_type=orm.Dict, + help="Settings for the PW.x calculations") + spec.input("qe.dft.supercell.pseudopotential_family", valid_type=orm.Str, + help="The pseudopotential family for use with the code") # DFT inputs (PW.x) for the unitcell calculation for the dielectric constant - spec.input("qe.dft.unitcell.code", - valid_type=orm.Code, + spec.input("qe.dft.unitcell.code", valid_type=orm.Code, help="The pw.x code to use for the calculations") - spec.input("qe.dft.unitcell.kpoints", - valid_type=orm.KpointsData, - help="The k-point grid to use for the calculations") spec.input("qe.dft.unitcell.parameters", - valid_type=orm.Dict, + valid_type=orm.Dict, required=False, help="Parameters for the PWSCF calcuations. Some will be set automatically") spec.input("qe.dft.unitcell.scheduler_options", valid_type=orm.Dict, help="Scheduler options for the PW.x calculations") - spec.input_namespace("qe.dft.unitcell.pseudopotentials", - valid_type=orm.UpfData, - dynamic=True, - help="The pseudopotential family for use with the code, if required") - + spec.input("qe.dft.unitcell.settings", valid_type=orm.Dict, + help="Settings for the PW.x calculations") + spec.input("qe.dft.unitcell.pseudopotential_family", valid_type=orm.Str, + help="The pseudopotential family for use with the code") + # Postprocessing inputs (PP.x) spec.input("qe.pp.code", valid_type=orm.Code, @@ -95,18 +113,24 @@ def define(cls, spec): spec.outline( cls.setup, + if_(cls.if_run_chem_pot_wc)( + cls.run_chemical_potential_workchain, + ), + cls.check_chemical_potential_workchain, if_(cls.correction_required)( if_(cls.is_gaussian_scheme)( cls.prep_dft_calcs_gaussian_correction, cls.check_dft_calcs_gaussian_correction, cls.get_dft_potentials_gaussian_correction, cls.check_dft_potentials_gaussian_correction, - if_(cls.host_unitcell_provided)( + cls.get_charge_density, + cls.check_charge_density_calculations, + if_(cls.if_run_dfpt)( cls.prep_hostcell_calc_for_dfpt, cls.check_hostcell_calc_for_dfpt, + cls.prep_calc_dfpt_calculation, ), - cls.prep_calc_dfpt_calculation, - cls.check_dfpt_calculation, + cls.get_permittivity, cls.run_gaussian_correction_workchain), if_(cls.is_point_scheme)( cls.raise_not_implemented @@ -125,123 +149,483 @@ def prep_dft_calcs_gaussian_correction(self): """ self.report("Setting up the Gaussian Countercharge correction workchain") + + relax_type = {'fixed': RelaxType.NONE, 'relax': RelaxType.POSITIONS, 'vc-relax': RelaxType.POSITIONS_CELL} + + overrides = { + 'base':{ + # 'pseudo_family': self.inputs.qe.dft.supercell.pseudopotential_family.value, + 'pw': { + 'parameters': {}, + # 'metadata': self.inputs.qe.dft.supercell.scheduler_options.get_dict(), + 'settings': self.inputs.qe.dft.supercell.settings.get_dict(), + } + }, + 'base_final_scf':{ + # 'pseudo_family': self.inputs.qe.dft.supercell.pseudopotential_family.value, + 'pw': { + 'parameters': {}, + # 'metadata': self.inputs.qe.dft.supercell.scheduler_options.get_dict(), + 'settings': self.inputs.qe.dft.supercell.settings.get_dict(), + } + }, + 'clean_workdir' : orm.Bool(False), + } + + if 'pseudopotential_family' in self.inputs.qe.dft.supercell: + overrides['base']['pseudo_family'] = self.inputs.qe.dft.supercell.pseudopotential_family.value + overrides['base_final_scf']['pseudo_family'] = self.inputs.qe.dft.supercell.pseudopotential_family.value + if 'parameters' in self.inputs.qe.dft.supercell: + overrides['base']['pw']['parameters'] = self.inputs.qe.dft.supercell.parameters.get_dict() + overrides['base_final_scf']['pw']['parameters'] = self.inputs.qe.dft.supercell.parameters.get_dict() + # else: + # overrides['base']['pw']['parameters'] = {} + # overrides['base_final_scf']['pw']['parameters'] = {} - pw_inputs = self.inputs.qe.dft.supercell.code.get_builder() - pw_inputs.pseudos = self.inputs.qe.dft.supercell.pseudopotentials - pw_inputs.kpoints = self.inputs.qe.dft.supercell.kpoints - pw_inputs.metadata = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + # Host structure + if self.inputs.run_pw_host: + inputs = PwRelaxWorkChain.get_builder_from_protocol( + code = self.inputs.qe.dft.supercell.code, + structure = self.inputs.host_structure, + overrides = overrides, + relax_type = relax_type[self.inputs.relaxation_scheme.value] + ) + + inputs['base']['pw']['metadata'] = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + inputs['base']['pw']['settings'] = self.inputs.qe.dft.supercell.settings + inputs['base_final_scf']['pw']['metadata'] = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + inputs['base_final_scf']['pw']['settings'] = self.inputs.qe.dft.supercell.settings + + #future = self.submit(PwRelaxWorkChain, **inputs) + future = self.submit(inputs) + self.report( + 'Launching PWSCF for the host structure (PK={}) with charge {} (PK={})' + .format(self.inputs.host_structure.pk, "0.0", future.pk)) + self.to_context(**{'calc_host': future}) - parameters = self.inputs.qe.dft.supercell.parameters.get_dict() + # Defect structure; neutral charge state + if self.inputs.run_pw_defect_q0: + inputs = PwRelaxWorkChain.get_builder_from_protocol( + code = self.inputs.qe.dft.supercell.code, + structure = self.inputs.defect_structure, + overrides = overrides, + relax_type = relax_type[self.inputs.relaxation_scheme.value] + ) + + inputs['base']['pw']['metadata'] = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + inputs['base']['pw']['settings'] = self.inputs.qe.dft.supercell.settings + inputs['base_final_scf']['pw']['metadata'] = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + inputs['base_final_scf']['pw']['settings'] = self.inputs.qe.dft.supercell.settings + + #future = self.submit(PwRelaxWorkChain, **inputs) + future = self.submit(inputs) + self.report( + 'Launching PWSCF for the defect structure (PK={}) with charge {} (PK={})' + .format(self.inputs.defect_structure.pk, "0.0", future.pk)) + self.to_context(**{'calc_defect_q0': future}) - # We set 'tot_charge' later so throw an error if the user tries to set it to avoid - # any ambiguity or unseen modification of user input - if 'tot_charge' in parameters['SYSTEM']: - self.report('You cannot set the "tot_charge" PW.x parameter explicitly') - return self.exit_codes.ERROR_PARAMETER_OVERRIDE + # Defect structure; target charge state + if self.inputs.run_pw_defect_q: + overrides['base']['pw']['parameters'] = recursive_merge(overrides['base']['pw']['parameters'], {'SYSTEM':{'tot_charge': self.inputs.defect_charge.value}}) + overrides['base_final_scf']['pw']['parameters'] = recursive_merge(overrides['base_final_scf']['pw']['parameters'], {'SYSTEM':{'tot_charge': self.inputs.defect_charge.value}}) + + inputs = PwRelaxWorkChain.get_builder_from_protocol( + code = self.inputs.qe.dft.supercell.code, + structure = self.inputs.defect_structure, + overrides = overrides, + relax_type = relax_type[self.inputs.relaxation_scheme.value] + ) + + inputs['base']['pw']['metadata'] = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + inputs['base']['pw']['settings'] = self.inputs.qe.dft.supercell.settings + inputs['base_final_scf']['pw']['metadata'] = self.inputs.qe.dft.supercell.scheduler_options.get_dict() + inputs['base_final_scf']['pw']['settings'] = self.inputs.qe.dft.supercell.settings + + #future = self.submit(PwRelaxWorkChain, **inputs) + future = self.submit(inputs) + self.report( + 'Launching PWSCF for the defect structure (PK={}) with charge {} (PK={})' + .format(self.inputs.defect_structure.pk, self.inputs.defect_charge.value, future.pk)) + self.to_context(**{'calc_defect_q': future}) + + def check_dft_calcs_gaussian_correction(self): + """ + Check if the required calculations for the Gaussian Countercharge correction workchain + have finished correctly. + """ - # Host structure - pw_inputs.structure = self.inputs.host_structure - parameters['SYSTEM']['tot_charge'] = orm.Float(0.) - pw_inputs.parameters = orm.Dict(dict=parameters) + # Host + if self.inputs.run_pw_host: + host_calc = self.ctx['calc_host'] + if host_calc.is_finished_ok: + self.ctx.host_energy = orm.Float(host_calc.outputs.output_parameters.get_dict()['energy']) # eV + self.report('The energy of the host is: {} eV'.format(self.ctx.host_energy.value)) + #self.ctx.host_vbm = orm.Float(host_calc.outputs.output_band.get_array('bands')[0][-1]) # valence band maximum + self.ctx.host_vbm = orm.Float(get_vbm(host_calc)) + self.report('The top of valence band is: {} eV'.format(self.ctx.host_vbm.value)) + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(host_calc.outputs.output_band) + if not is_insulator: + self.report('WARNING! The ground state of the host structure is metallic!') + else: + self.report( + 'PWSCF for the host structure has failed with status {}'.format(host_calc.exit_status)) + return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + else: + HostNode = orm.load_node(self.inputs.host_node.value) + self.ctx.host_energy = orm.Float(HostNode.outputs.output_parameters.get_dict()['energy']) # eV + self.report('Extracting PWSCF for host structure with charge {} from node PK={}' + .format("0.0", self.inputs.host_node.value)) + self.report('The energy of the host is: {} eV'.format(self.ctx.host_energy.value)) + #self.ctx.host_vbm = orm.Float(HostNode.outputs.output_band.get_array('bands')[0][-1]) # eV + self.ctx.host_vbm = orm.Float(get_vbm(HostNode)) + self.report('The top of valence band is: {} eV'.format(self.ctx.host_vbm.value)) + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(HostNode.outputs.output_band) + if not is_insulator: + self.report('WARNING! The ground state of the host structure is metallic!') + + # Defect (q=0) + if self.inputs.run_pw_defect_q0: + defect_q0_calc = self.ctx['calc_defect_q0'] + if not defect_q0_calc.is_finished_ok: + self.report('PWSCF for the defect structure (with charge 0) has failed with status {}'.format(defect_q0_calc.exit_status)) + return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + else: + self.report('The energy of neutral defect structure is: {} eV'.format(defect_q0_calc.outputs.output_parameters.get_dict()['energy'])) + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(defect_q0_calc.outputs.output_band) + if not is_insulator: + self.report('WARNING! The ground state of neutral defect structure is metallic!') + else: + Defect_q0Node = orm.load_node(self.inputs.defect_q0_node.value) + self.report('Extracting PWSCF for defect structure with charge {} from node PK={}'.format("0.0", self.inputs.defect_q0_node.value)) + self.report('The energy of neutral defect structure is: {} eV'.format(Defect_q0Node.outputs.output_parameters.get_dict()['energy'])) + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(Defect_q0Node.outputs.output_band) + if not is_insulator: + self.report('WARNING! The ground state of neutral defect structure is metallic!') - future = self.submit(pw_inputs) - self.report( - 'Launching PWSCF for host structure (PK={}) with charge {} (PK={})' - .format(self.inputs.host_structure.pk, "0.0", future.pk)) - self.to_context(**{'calc_host': future}) + # Defect (q=q) + if self.inputs.run_pw_defect_q: + defect_q_calc = self.ctx['calc_defect_q'] + if defect_q_calc.is_finished_ok: + self.ctx.defect_energy = orm.Float(defect_q_calc.outputs.output_parameters.get_dict()['energy']) # eV + self.report('The energy of defect structure with charge {} is: {} eV'. + format(self.inputs.defect_charge.value, defect_q_calc.outputs.output_parameters.get_dict()['energy'])) + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(defect_q_calc.outputs.output_band) + if not is_insulator: + self.report('WARNING! The ground state of charged defect structure is metallic!') + else: + self.report( + 'PWSCF for the defect structure (with charge {}) has failed with status {}' + .format(self.inputs.defect_charge.value, defect_q_calc.exit_status)) + return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + else: + Defect_qNode = orm.load_node(self.inputs.defect_q_node.value) + self.report('Extracting PWSCF for defect structure with charge {} from node PK={}' + .format(self.inputs.defect_charge.value, self.inputs.defect_q_node.value)) + self.ctx.defect_energy = orm.Float(Defect_qNode.outputs.output_parameters.get_dict()['energy']) # eV + self.report('The energy of defect structure with charge {} is: {} eV'. + format(self.inputs.defect_charge.value, Defect_qNode.outputs.output_parameters.get_dict()['energy'])) + is_insulator, band_gap = orm.nodes.data.array.bands.find_bandgap(Defect_qNode.outputs.output_band) + if not is_insulator: + self.report('WARNING! The ground state of charged defect structure is metallic!') - # Defect structure; neutral charge state - pw_inputs.structure = self.inputs.defect_structure - parameters['SYSTEM']['tot_charge'] = orm.Float(0.) - pw_inputs.parameters = orm.Dict(dict=parameters) + def get_dft_potentials_gaussian_correction(self): + """ + Obtain the electrostatic potentials from the PWSCF calculations. + """ + + # User inputs + pp_inputs = PpCalculation.get_builder() + pp_inputs.code = self.inputs.qe.pp.code + pp_inputs.metadata = self.inputs.qe.pp.scheduler_options.get_dict() - future = self.submit(pw_inputs) - self.report( - 'Launching PWSCF for defect structure (PK={}) with charge {} (PK={})' - .format(self.inputs.defect_structure.pk, "0.0", future.pk)) - self.to_context(**{'calc_defect_q0': future}) + # Fixed settings + #pp_inputs.plot_number = orm.Int(0) # Charge density + #pp_inputs.plot_dimension = orm.Int(3) # 3D - # Defect structure; target charge state - pw_inputs.structure = self.inputs.defect_structure - parameters['SYSTEM']['tot_charge'] = self.inputs.defect_charge - pw_inputs.parameters = orm.Dict(dict=parameters) + parameters = orm.Dict(dict={ + 'INPUTPP': { + "plot_num" : 11, + }, + 'PLOT': { + "iflag" : 3 + } + }) + pp_inputs.parameters = parameters - future = self.submit(pw_inputs) - self.report( - 'Launching PWSCF for defect structure (PK={}) with charge {} (PK={})' - .format(self.inputs.defect_structure.pk, - self.inputs.defect_charge.value, future.pk)) - self.to_context(**{'calc_defect_q': future}) + # Host +# if self.inputs.run_pw_host: +# pp_inputs.parent_folder = self.ctx['calc_host'].outputs.remote_folder +# else: +# HostNode = orm.load_node(int(self.inputs.host_node)) +# pp_inputs.parent_folder = HostNode.outputs.remote_folder + if self.inputs.run_v_host: + if self.inputs.run_pw_host: + pp_inputs.parent_folder = self.ctx['calc_host'].outputs.remote_folder + else: + temp_node = orm.load_node(self.inputs.host_node.value) + pp_inputs.parent_folder = temp_node.outputs.remote_folder + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for host structure (PK={}) with charge {} (PK={})'. + format(self.inputs.host_structure.pk, "0.0", future.pk)) + self.to_context(**{'calc_v_host': future}) + else: + self.ctx['calc_v_host'] = orm.load_node(self.inputs.v_host_node.value) + # Defect (q=0) + if self.inputs.run_v_defect_q0: + if self.inputs.run_pw_defect_q0: + pp_inputs.parent_folder = self.ctx['calc_defect_q0'].outputs.remote_folder + else: + temp_node = orm.load_node(self.inputs.defect_q0_node.value) + pp_inputs.parent_folder = temp_node.outputs.remote_folder + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for defect structure (PK={}) with charge {} (PK={})' + .format(self.inputs.defect_structure.pk, "0.0", future.pk)) + self.to_context(**{'calc_v_defect_q0': future}) + else: + self.ctx['calc_v_defect_q0'] = orm.load_node(self.inputs.v_defect_q0_node.value) - def check_dft_calcs_gaussian_correction(self): + + # Defect (q=q) + if self.inputs.run_v_defect_q: + if self.inputs.run_pw_defect_q: + pp_inputs.parent_folder = self.ctx['calc_defect_q'].outputs.remote_folder + else: + temp_node = orm.load_node(self.inputs.defect_q_node.value) + pp_inputs.parent_folder = temp_node.outputs.remote_folder + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for defect structure (PK={}) with charge {} (PK={})' + .format(self.inputs.defect_structure.pk, self.inputs.defect_charge.value, future.pk)) + self.to_context(**{'calc_v_defect_q': future}) + else: + self.ctx['calc_v_defect_q'] = orm.load_node(self.inputs.v_defect_q_node.value) + + def check_dft_potentials_gaussian_correction(self): """ Check if the required calculations for the Gaussian Countercharge correction workchain have finished correctly. """ # Host - host_calc = self.ctx['calc_host'] - if host_calc.is_finished_ok: - self.ctx.host_energy = orm.Float(host_calc.outputs.output_parameters.get_dict()['energy']) # eV - self.ctx.host_vbm = orm.Float(host_calc.outputs.output_band.get_array('bands')[0][-1]) # valence band maximum + host_pp = self.ctx['calc_v_host'] + if host_pp.is_finished_ok: + # data_array = host_pp.outputs.output_data.get_array('data') + # v_data = orm.ArrayData() + # v_data.set_array('data', data_array) + # self.ctx.v_host = v_data + # self.ctx.v_host = host_pp.outputs.output_data + self.ctx.v_host = get_data_array(host_pp.outputs.output_data) else: self.report( - 'PWSCF for the host structure has failed with status {}'. - format(host_calc.exit_status)) - return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + 'Post processing for the host structure has failed with status {}'.format(host_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED # Defect (q=0) - defect_q0_calc = self.ctx['calc_defect_q0'] - if not defect_q0_calc.is_finished_ok: + defect_q0_pp = self.ctx['calc_v_defect_q0'] + if defect_q0_pp.is_finished_ok: + # data_array = defect_q0_pp.outputs.output_data.get_array('data') + # v_data = orm.ArrayData() + # v_data.set_array('data', data_array) + # self.ctx.v_defect_q0 = v_data + # self.ctx.v_defect_q0 = defect_q0_pp.outputs.output_data + self.ctx.v_defect_q0 = get_data_array(defect_q0_pp.outputs.output_data) + else: self.report( - 'PWSCF for the defect structure (with charge 0) has failed with status {}' - .format(defect_q0_calc.exit_status)) - return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + 'Post processing for the defect structure (with charge 0) has failed with status {}' + .format(defect_q0_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED # Defect (q=q) - defect_q_calc = self.ctx['calc_defect_q'] - if defect_q_calc.is_finished_ok: - self.ctx.defect_energy = orm.Float(defect_q_calc.outputs.output_parameters.get_dict()['energy']) # eV + defect_q_pp = self.ctx['calc_v_defect_q'] + if defect_q_pp.is_finished_ok: + # data_array = defect_q_pp.outputs.output_data.get_array('data') + # v_data = orm.ArrayData() + # v_data.set_array('data', data_array) + # self.ctx.v_defect_q = v_data + # self.ctx.v_defect_q = defect_q_pp.outputs.output_data + self.ctx.v_defect_q = get_data_array(defect_q_pp.outputs.output_data) else: self.report( - 'PWSCF for the defect structure (with charge {}) has failed with status {}' - .format(self.inputs.defect_charge.value, - defect_q_calc.exit_status)) - return self.exit_codes.ERROR_DFT_CALCULATION_FAILED + 'Post processing for the defect structure (with charge {}) has failed with status {}' + .format(self.inputs.defect_charge.value,defect_q_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED + + def get_charge_density(self): + """ + Obtain the electrostatic potentials from the PWSCF calculations. + """ + + # User inputs + pp_inputs = PpCalculation.get_builder() + pp_inputs.code = self.inputs.qe.pp.code + pp_inputs.metadata = self.inputs.qe.pp.scheduler_options.get_dict() + + # Fixed settings + #pp_inputs.plot_number = orm.Int(0) # Charge density + #pp_inputs.plot_dimension = orm.Int(3) # 3D + + parameters = orm.Dict(dict={ + 'INPUTPP': { + "plot_num" : 0, + }, + 'PLOT': { + "iflag" : 3 + } + }) + pp_inputs.parameters = parameters + + # Host + if self.inputs.run_rho_host: + if self.inputs.run_pw_host: + pp_inputs.parent_folder = self.ctx['calc_host'].outputs.remote_folder + else: + temp_node = orm.load_node(self.inputs.host_node.value) + pp_inputs.parent_folder = temp_node.outputs.remote_folder + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for charge density of host structure (PK={}) with charge {} (PK={})' + .format(self.inputs.host_structure.pk, "0.0", future.pk)) + self.to_context(**{'calc_rho_host': future}) + else: + self.ctx['calc_rho_host'] = orm.load_node(self.inputs.rho_host_node.value) + + # Defect (q=0) + if self.inputs.run_rho_defect_q0: + if self.inputs.run_pw_defect_q0: + pp_inputs.parent_folder = self.ctx['calc_defect_q0'].outputs.remote_folder + else: + temp_node = orm.load_node(self.inputs.defect_q0_node.value) + pp_inputs.parent_folder = temp_node.outputs.remote_folder + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for charge density of defect structure (PK={}) with charge {} (PK={})' + .format(self.inputs.defect_structure.pk, "0.0", future.pk)) + self.to_context(**{'calc_rho_defect_q0': future}) + else: + self.ctx['calc_rho_defect_q0'] = orm.load_node(self.inputs.rho_defect_q0_node.value) + + # Defect (q=q) + if self.inputs.run_rho_defect_q: + if self.inputs.run_pw_defect_q: + pp_inputs.parent_folder = self.ctx['calc_defect_q'].outputs.remote_folder + else: + temp_node = orm.load_node(self.inputs.defect_q_node.value) + pp_inputs.parent_folder = temp_node.outputs.remote_folder + future = self.submit(PpCalculation, **pp_inputs) + self.report('Launching PP.x for charge density of defect structure (PK={}) with charge {} (PK={})' + .format(self.inputs.defect_structure.pk, self.inputs.defect_charge.value, future.pk)) + self.to_context(**{'calc_rho_defect_q': future}) + else: + self.ctx['calc_rho_defect_q'] = orm.load_node(self.inputs.rho_defect_q_node.value) + + def check_charge_density_calculations(self): + """ + Check if the required calculations for the Gaussian Countercharge correction workchain + have finished correctly. + """ + + # Host + host_pp = self.ctx['calc_rho_host'] + if host_pp.is_finished_ok: + # data_array = host_pp.outputs.output_data.get_array('data') + # v_data = orm.ArrayData() + # v_data.set_array('data', data_array) + # self.ctx.rho_host = v_data + # self.ctx.rho_host = host_pp.outputs.output_data + self.ctx.rho_host = get_data_array(host_pp.outputs.output_data) + else: + self.report( + 'Post processing for the host structure has failed with status {}'.format(host_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED + + # Defect (q=0) + defect_q0_pp = self.ctx['calc_rho_defect_q0'] + if defect_q0_pp.is_finished_ok: + # data_array = defect_q0_pp.outputs.output_data.get_array('data') + # v_data = orm.ArrayData() + # v_data.set_array('data', data_array) + # self.ctx.rho_defect_q0 = v_data + # self.ctx.rho_defect_q0 = defect_q0_pp.outputs.output_data + self.ctx.rho_defect_q0 = get_data_array(defect_q0_pp.outputs.output_data) + else: + self.report( + 'Post processing for the defect structure (with charge 0) has failed with status {}' + .format(defect_q0_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED + + # Defect (q=q) + defect_q_pp = self.ctx['calc_rho_defect_q'] + if defect_q_pp.is_finished_ok: + # data_array = defect_q_pp.outputs.output_data.get_array('data') + # v_data = orm.ArrayData() + # v_data.set_array('data', data_array) + # self.ctx.rho_defect_q = v_data + # self.ctx.rho_defect_q = defect_q_pp.outputs.output_data + self.ctx.rho_defect_q = get_data_array(defect_q_pp.outputs.output_data) + else: + self.report( + 'Post processing for the defect structure (with charge 0) has failed with status {}' + .format(defect_q_pp.exit_status)) + return self.exit_codes.ERROR_PP_CALCULATION_FAILED def prep_hostcell_calc_for_dfpt(self): """ Run a DFT calculation on the structure to be used for the computation of the dielectric constant """ - self.report("An alternative unit cell has been requested") # Another code may be desirable - N.B. in AiiDA a code refers to a specific # executable on a specific computer. As the PH calculation may have to be run on # an HPC cluster, the PW calculation must be run on the same machine and so this # may necessitate that a different code is used than that for the supercell calculations. - pw_inputs = self.inputs.qe.dft.unitcell.code.get_builder() - - # These are not necessarily the same as for the other DFT calculations - pw_inputs.pseudos = self.inputs.qe.dft.unitcell.pseudopotentials - pw_inputs.kpoints = self.inputs.qe.dft.unitcell.kpoints - pw_inputs.metadata = self.inputs.qe.dft.unitcell.scheduler_options.get_dict() - - pw_inputs.structure = self.inputs.host_unitcell - parameters = self.inputs.qe.dft.unitcell.parameters.get_dict() - pw_inputs.parameters = orm.Dict(dict=parameters) - future = self.submit(pw_inputs) + relax_type = {'fixed': RelaxType.NONE, 'relax': RelaxType.POSITIONS, 'vc-relax': RelaxType.POSITIONS_CELL} + + overrides = { + 'base':{ + # 'pseudo_family': self.inputs.qe.dft.unitcell.pseudopotential_family.value, + 'pw': { + 'parameters': {}, + # 'metadata': self.inputs.qe.dft.unitcell.scheduler_options.get_dict(), + 'settings': self.inputs.qe.dft.unitcell.settings.get_dict(), + } + }, + 'base_final_scf':{ + # 'pseudo_family': self.inputs.qe.dft.unitcell.pseudopotential_family.value, + 'pw': { + 'parameters': {}, + # 'metadata': self.inputs.qe.dft.unitcell.scheduler_options.get_dict(), + 'settings': self.inputs.qe.dft.unitcell.settings.get_dict(), + } + }, + 'clean_workdir' : orm.Bool(False), + } + + if 'pseudopotential_family' in self.inputs.qe.dft.unitcell: + overrides['base']['pseudo_family'] = self.inputs.qe.dft.unitcell.pseudopotential_family.value + overrides['base_final_scf']['pseudo_family'] = self.inputs.qe.dft.unitcell.pseudopotential_family.value + if 'parameters' in self.inputs.qe.dft.unitcell: + overrides['base']['pw']['parameters'] = self.inputs.qe.dft.unitcell.parameters.get_dict() + overrides['base_final_scf']['pw']['parameters'] = self.inputs.qe.dft.unitcell.parameters.get_dict() + + inputs = PwRelaxWorkChain.get_builder_from_protocol( + code = self.inputs.qe.dft.unitcell.code, + structure = self.inputs.host_unitcell, + overrides = overrides, + relax_type = relax_type[self.inputs.relaxation_scheme.value] + ) + + inputs['base']['pw']['metadata'] = self.inputs.qe.dft.unitcell.scheduler_options.get_dict() + inputs['base']['pw']['settings'] = self.inputs.qe.dft.unitcell.settings + inputs['base_final_scf']['pw']['metadata'] = self.inputs.qe.dft.unitcell.scheduler_options.get_dict() + inputs['base_final_scf']['pw']['settings'] = self.inputs.qe.dft.unitcell.settings + + #future = self.submit(PwRelaxWorkChain, **inputs) + future = self.submit(inputs) self.report( - 'Launching PWSCF for host unitcell structure (PK={})' - .format(self.inputs.host_structure.pk, future.pk) - ) + 'Launching PWSCF for host unitcell structure (PK={}) at node (PK={})'. + format(self.inputs.host_unitcell.pk, future.pk)) self.to_context(**{'calc_host_unitcell': future}) - return - def check_hostcell_calc_for_dfpt(self): """ Check if the DFT calculation to be used for the computation of the @@ -292,106 +676,23 @@ def prep_calc_dfpt_calculation(self): ph_inputs.metadata = self.inputs.qe.dfpt.scheduler_options.get_dict() future = self.submit(ph_inputs) - self.report('Launching PH for host structure (PK={})'.format( - self.inputs.host_structure.pk, future.pk)) + self.report('Launching PH for host structure (PK={})'.format(self.inputs.host_structure.pk, future.pk)) self.to_context(**{'calc_dfpt': future}) - def check_dfpt_calculation(self): - - """ - Check that the DFPT calculation has completed successfully - """ - dfpt_calc = self.ctx['calc_dfpt'] - - if dfpt_calc.is_finished_ok: - epsilion_tensor = np.array(dfpt_calc.outputs.output_parameters.get_dict()['dielectric_constant']) - self.ctx.epsilon = orm.Float(np.trace(epsilion_tensor/3.)) - self.report('The computed relative permittivity is {}'.format( - self.ctx.epsilon.value)) - else: - self.report( - 'PH for the host structure has failed with status {}'.format(dfpt_calc.exit_status)) - return self.exit_codes.ERROR_DFPT_CALCULATION_FAILED - - def get_dft_potentials_gaussian_correction(self): - """ - Obtain the electrostatic potentials from the PWSCF calculations. - """ - - # User inputs - pp_inputs = self.inputs.qe.pp.code.get_builder() - pp_inputs.metadata = self.inputs.qe.pp.scheduler_options.get_dict() - - # Fixed settings - pp_inputs.plot_number = orm.Int(11) # Elctrostatic potential - pp_inputs.plot_dimension = orm.Int(3) # 3D - - pp_inputs.parent_folder = self.ctx['calc_host'].outputs.remote_folder - future = self.submit(pp_inputs) - self.report( - 'Launching PP.x for host structure (PK={}) with charge {} (PK={})'. - format(self.inputs.host_structure.pk, "0.0", future.pk)) - self.to_context(**{'pp_host': future}) - - pp_inputs.parent_folder = self.ctx[ - 'calc_defect_q0'].outputs.remote_folder - future = self.submit(pp_inputs) - self.report( - 'Launching PP.x for defect structure (PK={}) with charge {} (PK={})' - .format(self.inputs.defect_structure.pk, "0.0", future.pk)) - self.to_context(**{'pp_defect_q0': future}) - - pp_inputs.parent_folder = self.ctx[ - 'calc_defect_q'].outputs.remote_folder - future = self.submit(pp_inputs) - self.report( - 'Launching PP.x for defect structure (PK={}) with charge {} (PK={})' - .format(self.inputs.defect_structure.pk, - self.inputs.defect_charge.value, future.pk)) - self.to_context(**{'pp_defect_q': future}) - - def check_dft_potentials_gaussian_correction(self): + def get_permittivity(self): """ - Check if the required calculations for the Gaussian Countercharge correction workchain - have finished correctly. + Compute the dielectric constant to be used in the correction """ - - # Host - host_pp = self.ctx['pp_host'] - if host_pp.is_finished_ok: - data_array = host_pp.outputs.output_data.get_array('data') - v_data = orm.ArrayData() - v_data.set_array('data', data_array) - self.ctx.v_host = v_data - else: - self.report( - 'Post processing for the host structure has failed with status {}' - .format(host_pp.exit_status)) - return self.exit_codes.ERROR_PP_CALCULATION_FAILED - - # Defect (q=0) - defect_q0_pp = self.ctx['pp_defect_q0'] - if defect_q0_pp.is_finished_ok: - data_array = host_pp.outputs.output_data.get_array('data') - v_data = orm.ArrayData() - v_data.set_array('data', data_array) - self.ctx.v_defect_q0 = v_data + if self.inputs.run_dfpt: + dfpt_calc = self.ctx['calc_dfpt'] + if dfpt_calc.is_finished_ok: + epsilion_tensor = np.array(dfpt_calc.outputs.output_parameters.get_dict()['dielectric_constant']) + self.ctx.epsilon = orm.Float(np.trace(epsilion_tensor/3.)) + self.report('The computed relative permittivity is {}'.format(self.ctx.epsilon.value)) + else: + self.report( + 'PH for the host structure has failed with status {}'.format(dfpt_calc.exit_status)) + return self.exit_codes.ERROR_DFPT_CALCULATION_FAILED else: - self.report( - 'Post processing for the defect structure (with charge 0) has failed with status {}' - .format(defect_q0_pp.exit_status)) - return self.exit_codes.ERROR_PP_CALCULATION_FAILED + self.ctx.epsilon = self.inputs.epsilon - # Defect (q=q) - defect_q_pp = self.ctx['pp_defect_q'] - if defect_q_pp.is_finished_ok: - data_array = host_pp.outputs.output_data.get_array('data') - v_data = orm.ArrayData() - v_data.set_array('data', data_array) - self.ctx.v_defect_q = v_data - else: - self.report( - 'Post processing for the defect structure (with charge {}) has failed with status {}' - .format(self.inputs.defect_charge.value, - defect_q_pp.exit_status)) - return self.exit_codes.ERROR_PP_CALCULATION_FAILED diff --git a/aiida_defects/formation_energy/formation_energy_siesta.py b/aiida_defects/formation_energy/formation_energy_siesta.py new file mode 100644 index 0000000..5ff8934 --- /dev/null +++ b/aiida_defects/formation_energy/formation_energy_siesta.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +import numpy as np + +from aiida import orm +from aiida.engine import WorkChain, calcfunction, ToContext, if_, submit +from aiida.plugins import WorkflowFactory +from aiida_quantumespresso.workflows.pw.base import PwBaseWorkChain + +from aiida_defects.formation_energy.formation_energy_base import FormationEnergyWorkchainBase +from aiida_defects.formation_energy.utils import run_pw_calculation +from .utils import get_raw_formation_energy, get_corrected_formation_energy, get_corrected_aligned_formation_energy + + +class FormationEnergyWorkchainSiesta(FormationEnergyWorkchainBase): + """ + Compute the formation energy for a given defect using Siesta + """ + @classmethod + def define(cls, spec): + super(FormationEnergyWorkchainSiesta, cls).define(spec) + + # Namespace to make it clear which code is being used. + spec.input_namespace('siesta.dft.supercell', + help="Inputs for DFT calculations on supercells") + + # DFT inputs (Siesta) + # spec.input("siesta.dft.supercell.code", + # valid_type=orm.Code, + # help="The Siesta code to use for the supercell calculations") + # spec.input("siesta.dft.supercell.parameters", + # valid_type=orm.Dict, + # help="Parameters for the supercell calculations. Some will be set automatically") + # spec.input("siesta.dft.supercell.scheduler_options", + # valid_type=orm.Dict, + # help="Scheduler options for the Siesta calculation") + + spec.outline( + cls.setup, + if_(cls.correction_required)( + if_(cls.is_gaussian_scheme)( + cls.placeholder, + cls.run_gaussian_correction_workchain), + if_(cls.is_point_scheme)( + cls.raise_not_implemented + #cls.prepare_point_correction_workchain, + #cls.run_point_correction_workchain), + ), + cls.check_correction_workchain), + cls.compute_formation_energy + ) + + def placeholder(self): + """ + Placeholder method + """ + pass \ No newline at end of file diff --git a/aiida_defects/formation_energy/potential_alignment/density_weighted/density_weighted.py b/aiida_defects/formation_energy/potential_alignment/density_weighted/density_weighted.py new file mode 100644 index 0000000..6477f5f --- /dev/null +++ b/aiida_defects/formation_energy/potential_alignment/density_weighted/density_weighted.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +from aiida import orm +from aiida.engine import WorkChain, calcfunction + +from aiida_defects.formation_energy.potential_alignment.utils import get_potential_difference +from .utils import get_alignment, AllValuesMaskedError + + +class DensityWeightedAlignmentWorkchain(WorkChain): + """ + Comput the alignment needed between two electrostatic potentials according to + the charge-weighted potential alignment method. + """ + + @classmethod + def define(cls, spec): + super(DensityWeightedAlignmentWorkchain, cls).define(spec) + spec.input('first_potential', + valid_type=orm.ArrayData, + help="The first electrostatic potential array") + spec.input('second_potential', + valid_type=orm.ArrayData, + help="The second electrostatic potential array") + spec.input('charge_density', + valid_type=orm.ArrayData, + help="The fitted model charge density array") + spec.input('tolerance', + valid_type=orm.Float, + default=lambda: orm.Float(1.0e-3), + help="The threshold for determining whether a given array element has charge density present") + + spec.outline( + cls.setup, + cls.compute_difference, + cls.calculate_alignment, + cls.results, + ) + #spec.expose_outputs(PwBaseWorkChain, exclude=('output_structure',)) + spec.output('alignment_required', + valid_type=orm.Float, + required=True, + help="The computed potential alignment required") + spec.output('potential_difference', + valid_type=orm.ArrayData, + required=True, + help="The unmasked difference in electrostatic potentials") + + # Exit codes + spec.exit_code(301, 'ERROR_ALL_VALUES_MASKED', + message='All values in the potential difference array were masked. ' + 'Try increasing the tolerance to include fewer elements from the charge density array.') + + + def setup(self): + pass + + + def compute_difference(self): + """ + Compute the difference of the two potentials + """ + + self.ctx.potential_difference = get_potential_difference( + first_potential = self.inputs.first_potential, + second_potential = self.inputs.second_potential + ) + + + def calculate_alignment(self): + """ + Compute the alignment + """ + + try: + self.ctx.alignment = get_alignment( + potential_difference = self.ctx.potential_difference, + charge_density = self.inputs.charge_density, + tolerance = self.inputs.tolerance + ) + except AllValuesMaskedError: + return self.exit_codes.ERROR_ALL_VALUES_MASKED + + + + def results(self): + """ + Pack the results + """ + self.out('alignment_required', self.ctx.alignment) + self.out('potential_difference', self.ctx.potential_difference) diff --git a/aiida_defects/formation_energy/potential_alignment/density_weighted/utils.py b/aiida_defects/formation_energy/potential_alignment/density_weighted/utils.py new file mode 100644 index 0000000..1ef436b --- /dev/null +++ b/aiida_defects/formation_energy/potential_alignment/density_weighted/utils.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +import numpy as np + +from aiida.engine import calcfunction +from aiida import orm +""" +Utility functions for the potential alignment workchain +""" + +class AllValuesMaskedError(ValueError): + """ + Error raised when no values are left after the masking procedure. + If one proceeds to compute averages using an array in which all values + are masked, the resulting object is an instance of 'numpy.ma.core.MaskedConstant' + which cannot be stored by AiiDA and is, in any case, meaningless. + """ + pass + +@calcfunction +def get_alignment(potential_difference, charge_density, tolerance): + """ + Compute the density-weighted potential alignment + """ + # Unpack + tol = tolerance.value + v_diff = potential_difference.get_array( + potential_difference.get_arraynames()[0]) + charge_density = charge_density.get_array( + charge_density.get_arraynames()[0]) + + # Get array mask based on elements' charge exceeding the tolerance. + mask = np.ma.greater(np.abs(charge_density), tol) + + # Apply this mask to the diff array + v_diff_masked = np.ma.masked_array(v_diff, mask=mask) + + # Check if any values are left after masking + if v_diff_masked.count() == 0: + raise AllValuesMaskedError + + # Compute average alignment + alignment = np.average(np.abs(v_diff_masked)) + + return orm.Float(alignment) \ No newline at end of file diff --git a/aiida_defects/formation_energy/potential_alignment/lany_zunger/lany_zunger.py b/aiida_defects/formation_energy/potential_alignment/lany_zunger/lany_zunger.py index 9257980..b018f07 100644 --- a/aiida_defects/formation_energy/potential_alignment/lany_zunger/lany_zunger.py +++ b/aiida_defects/formation_energy/potential_alignment/lany_zunger/lany_zunger.py @@ -15,7 +15,7 @@ # from aiida_defects.pp.fft_tools import avg_potential_at_core -class LanyZungerWorkchain(WorkChain): +class LanyZungerAlignmentWorkchain(WorkChain): """ Compute the electrostatic potential alignment via the Lany-Zunger method. See: S. Lany and A. Zunger, PRB 78, 235104 (2008) @@ -24,29 +24,21 @@ class LanyZungerWorkchain(WorkChain): @classmethod def define(cls, spec): - super(LanyZungerWorkchain, cls).define(spec) + super(LanyZungerAlignmentWorkchain, cls).define(spec) spec.input('bulk_structure', valid_type=orm.StructureData), - spec.input( 'e_tol', - valid_type=orm.Float(), - default=orm.Float(0.2), - help= - "Energy tolerance to decide which atoms to exclude to compute alignment" - ) - + valid_type=orm.Float, + default=lambda: orm.Float(0.2), + help="Energy tolerance to decide which atoms to exclude to compute alignment") spec.input('first_potential', valid_type=orm.ArrayData) spec.input('second_potential', valid_type=orm.ArrayData) spec.input( 'alignment_scheme', valid_type=orm.Str, - default=orm.Str('lany-zunger')) - spec.input('interpolate', valid_type=orm.Bool, default=orm.Bool(False)) + default=lambda: orm.Str('lany-zunger')) + spec.input('interpolate', valid_type=orm.Bool, default=lambda: orm.Bool(False)) spec.outline( - cls.setup, - cls.do_interpolation, - cls.calculate_alignment, - cls.results, ) #spec.expose_outputs(PwBaseWorkChain, exclude=('output_structure',)) spec.output('alignment_required', valid_type=orm.Float, required=True) diff --git a/aiida_defects/formation_energy/potential_alignment/mae/mae.py b/aiida_defects/formation_energy/potential_alignment/mae/mae.py new file mode 100644 index 0000000..10d8055 --- /dev/null +++ b/aiida_defects/formation_energy/potential_alignment/mae/mae.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +from aiida import orm +from aiida.engine import WorkChain, calcfunction + +from aiida_defects.formation_energy.potential_alignment.utils import get_potential_difference +from .utils import get_alignment, AllValuesMaskedError, convert_Hat_to_Ryd + + +class MaeAlignmentWorkchain(WorkChain): + """ + Compute the alignment needed between two electrostatic potentials. + Data points are included or excluded based on their distance from the defect site. + The largest possible sphere + + The root mean squared difference between two potentials is computed using: + \begin{equation} + x = \int \left| ( V_2 - V_1 + \Delta z ) \right| + \end{equation} + where: + * V_1 and V_2 are the potentials to align + * \Delta_z is the required alignment + + """ + + @classmethod + def define(cls, spec): + super(MaeAlignmentWorkchain, cls).define(spec) + spec.input('first_potential', + valid_type=orm.ArrayData, + help="The first electrostatic potential array") + spec.input('second_potential', + valid_type=orm.ArrayData, + help="The second electrostatic potential array") + spec.input("defect_site", + valid_type=orm.List, + help="Defect site position in crystal coordinates.") + + spec.outline( + cls.setup, + cls.compute_difference, + cls.calculate_alignment, + cls.results, + ) + spec.output('alignment_required', + valid_type=orm.Float, + required=True, + help="The computed potential alignment required") + spec.output('potential_difference', + valid_type=orm.ArrayData, + required=True, + help="The unmasked difference in electrostatic potentials") + + # Exit codes + spec.exit_code(301, 'ERROR_ALL_VALUES_MASKED', + message='All values in the potential difference array were masked. ' + 'Try increasing the tolerance to include fewer elements from the charge density array.') + + + def setup(self): + pass + + + def compute_difference(self): + """ + Compute the difference of the two potentials + """ + + ### Temporary solution to convert potential to the same unit, has to be redone properly. + ### The potentials generate by pp.x are in Rydberg while the model potential is in Hartree + if len(self.inputs.second_potential.get_arraynames()) == 1: + #v_model = orm.ArrayData() + #v_model.set_array('data',self.inputs.second_potential.get_array(self.inputs.second_potential.get_arraynames()[0])*-2.0) # Hartree to Ry unit of potential - This is dirty - need to harmonise units + v_model = convert_Hat_to_Ryd(self.inputs.second_potential) + else: + v_model = self.inputs.second_potential + + self.ctx.potential_difference = get_potential_difference( + first_potential = self.inputs.first_potential, + # second_potential = self.inputs.second_potential + second_potential = v_model + ) + + + def calculate_alignment(self): + """ + Compute the alignment + """ + try: + self.ctx.alignment = get_alignment( + potential_difference = self.ctx.potential_difference, + defect_site= self.inputs.defect_site + ) + except AllValuesMaskedError: + return self.exit_codes.ERROR_ALL_VALUES_MASKED + + + def results(self): + """ + Pack the results + """ + self.out('alignment_required', self.ctx.alignment) + self.out('potential_difference', self.ctx.potential_difference) diff --git a/aiida_defects/formation_energy/potential_alignment/mae/utils.py b/aiida_defects/formation_energy/potential_alignment/mae/utils.py new file mode 100644 index 0000000..a5a3b49 --- /dev/null +++ b/aiida_defects/formation_energy/potential_alignment/mae/utils.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +from __future__ import absolute_import + +import numpy as np +from qe_tools import CONSTANTS + +from aiida.engine import calcfunction +from aiida import orm + +from aiida_defects.utils import get_cell_matrix, get_grid + +""" +Utility functions for the potential alignment workchain +""" + +class AllValuesMaskedError(ValueError): + """ + Error raised when no values are left after the masking procedure. + If one proceeds to compute averages using an array in which all values + are masked, the resulting object is an instance of 'numpy.ma.core.MaskedConstant' + which cannot be stored by AiiDA and is, in any case, meaningless. + """ + pass + +@calcfunction +def convert_Hat_to_Ryd(potential): + v_model = orm.ArrayData() + v_model.set_array('data', potential.get_array(potential.get_arraynames()[0])*-2.0) + + return v_model + +@calcfunction +def get_alignment(potential_difference, defect_site, cutoff_radius=lambda: orm.Float(0.5)): + """ + Compute the mean-absolute error potential alignment + + Parameters + ---------- + potential_difference - numpy array + The difference in the electrostatic potentials to be aligned + defect_site - length 3 list, tuple or array + defect postion in crystal coordinates + cutoff_radius - float + distance cutoff from defect site in crystal coordinates. Coordinates + less than this distance are considered to be influenced by the defect + and are excluded from the alignment + """ + # Unpack ArrayData object + v_diff = potential_difference.get_array( + potential_difference.get_arraynames()[0]) + + # Generate a crystal grid of the same dimension as the data + ijk_array = get_grid(v_diff.shape, endpoint=False) + # Compute the distance from the defect site to every other. + distance_vectors = np.array(defect_site.get_list()).reshape(3,1) - ijk_array + # Apply minimum image + min_image_vectors = (distance_vectors - np.rint(distance_vectors)) + # Compute distances and reshape to match input data + distances = np.linalg.norm(min_image_vectors, axis=0).reshape(v_diff.shape) + + # In crystal coordinates, the maximum separation between interacting + # images is d=1 so look for coordinates at a distance of less than d=0.5. + # These are the coordinates within the shphere of interaction of the defect. + # Mask these and only compute the alignment the remaining, most distance points. + mask = np.ma.less(distances, cutoff_radius.value) + v_diff_masked = np.ma.masked_array(v_diff, mask=mask) + values_remaining = (v_diff_masked.count()/np.prod(v_diff.shape))*100.0 + print('{:.2f}% of values remain'.format(values_remaining)) + + # Check if any values are left after masking + if v_diff_masked.count() == 0: + raise AllValuesMaskedError + + fit_result = fit_potential(v_diff_masked) + alignment = -1.*fit_result.x*CONSTANTS.ry_to_ev + + return orm.Float(alignment) + + +def fit_potential(v_diff): + """ + Find the offset between two potentials, delta_z, that minimises the summed absolute error. + """ + from scipy.optimize import minimize + + def obj(delta_z): + """ + Objective function. Delta_z is the alignment of potentials + """ + return np.sum(np.abs(v_diff-delta_z)) + + initial_guess = 1.0 + result = minimize(obj, initial_guess) + return result diff --git a/aiida_defects/formation_energy/potential_alignment/potential_alignment.py b/aiida_defects/formation_energy/potential_alignment/potential_alignment.py index 2e57f73..88fd9dd 100644 --- a/aiida_defects/formation_energy/potential_alignment/potential_alignment.py +++ b/aiida_defects/formation_energy/potential_alignment/potential_alignment.py @@ -7,15 +7,24 @@ ######################################################################################## from __future__ import absolute_import +import numpy as np + from aiida import orm -from aiida.engine import WorkChain, calcfunction -from aiida_defects.formation_energy.potential_alignment.lany_zunger import lany_zunger +from aiida.common import AttributeDict +from aiida.engine import WorkChain, calcfunction, if_ +from qe_tools import CONSTANTS +from .utils import get_interpolation +from .lany_zunger.lany_zunger import LanyZungerAlignmentWorkchain +from .density_weighted.density_weighted import DensityWeightedAlignmentWorkchain +from .mae.mae import MaeAlignmentWorkchain -@calcfunction -def testing(): - return orm.Float(0.0) +valid_schemes = { + 'lany_zunger' : LanyZungerAlignmentWorkchain, + 'density_weighted': DensityWeightedAlignmentWorkchain, + 'mae': MaeAlignmentWorkchain +} class PotentialAlignmentWorkchain(WorkChain): """ @@ -25,76 +34,170 @@ class PotentialAlignmentWorkchain(WorkChain): @classmethod def define(cls, spec): super(PotentialAlignmentWorkchain, cls).define(spec) - spec.input('first_potential', valid_type=orm.ArrayData) - spec.input('second_potential', valid_type=orm.ArrayData) - spec.input( - 'alignment_scheme', - valid_type=orm.Str, - default=orm.Str('lany-zunger')) - spec.input('interpolate', valid_type=orm.Bool, default=orm.Bool(False)) + spec.input('allow_interpolation', + valid_type=orm.Bool, + default=lambda: orm.Bool(False), + help="Whether to allow arrays of different shapes to be interpolated") + spec.expose_inputs(DensityWeightedAlignmentWorkchain, + namespace='density_weighted', + namespace_options={'required': False, 'populate_defaults': False}) + spec.expose_inputs(MaeAlignmentWorkchain, + namespace='mae', + namespace_options={'required': False, 'populate_defaults': False}) + spec.expose_inputs(LanyZungerAlignmentWorkchain, + namespace='lany_zunger', + namespace_options={'required': False, 'populate_defaults': False}) + spec.outline( cls.setup, - cls.do_interpolation, + if_(cls.interpolation_required)( + cls.do_interpolation, + ), cls.calculate_alignment, + cls.check_alignment_workchain, cls.results, ) #spec.expose_outputs(PwBaseWorkChain, exclude=('output_structure',)) spec.output('alignment_required', valid_type=orm.Float, required=True) + # Exit codes - spec.exit_code( - 401, - 'ERROR_SUB_PROCESS_FAILED_WRONG_SHAPE', - message= - 'the two electrostatic potentials must be the same shape, unless interpolation is allowed' - ) - spec.exit_code( - 402, - 'ERROR_SUB_PROCESS_FAILED_INTERPOLATION', - message='the interpolation could not be completed') - spec.exit_code( - 403, - 'ERROR_SUB_PROCESS_FAILED_BAD_SCHEME', - message='the alignment scheme requested is unknown') + spec.exit_code(201, 'ERROR_INPUT_BAD_SCHEME', + message='the alignment scheme requested is unknown.') + spec.exit_code(202, 'ERROR_INPUT_NO_SCHEME', + message='no alignment scheme was setup.') + spec.exit_code(203, 'ERROR_INPUT_EXTRA_ARRAYS', + message='an ArrayData object has more than one array packed inside.') + spec.exit_code(204, 'ERROR_INPUT_WRONG_SHAPE', + message='all input arrays must have the same shape, unless interpolation is allowed.') + spec.exit_code(205, 'ERROR_INPUT_WRONG_ASPECT_RATIO', + message='all input arrays must have the same aspect ratio for interpolation to be effective.') + spec.exit_code(301, 'ERROR_SUB_PROCESS_FAILED_INTERPOLATION', + message='the interpolation could not be completed.') + spec.exit_code(302, 'ERROR_SUB_PROCESS_FAILED_ALIGNMENT', + message='the potential alignment could not be completed.') + spec.exit_code(999, "ERROR_NOT_IMPLEMENTED", + message="The requested method is not yet implemented.") def setup(self): """ Input validation and context setup """ - # Two potentials need have the same shape - first_potential_shape = self.inputs.first_potential.get_shape( - self.inputs.first_potential.get_arraynames()[0]) - second_potential_shape = self.inputs.second_potential.get_shape( - self.inputs.second_potential.get_arraynames()[0]) - if first_potential_shape != second_potential_shape: - if self.inputs.interpolate: - self.ctx.interpolation_required = True - else: - self.report( - 'The two potentials could not be aligned as they are the different shapes and interpolation is not allowed.' - ) - return self.exit_codes.ERROR_SUB_PROCESS_FAILED_WRONG_SHAPE - else: - self.ctx.interpolation_required = False - if self.inputs.alignment_scheme not in ['lany-zunger']: - self.report( - 'The requested alignment scheme, "{}" is not recognised.'. - format(self.inputs.alignment_scheme)) - return self.exit_codes.ERROR_SUB_PROCESS_FAILED_BAD_SCHEME - - self.ctx.first_potential = self.inputs.first_potential - self.ctx.second_potential = self.inputs.second_potential + # Only one namespace should be used at a time, and only one + schemes_found = [] + for namespace in valid_schemes: + if namespace in self.inputs: + schemes_found.append(namespace) + if len(schemes_found) == 1: + self.ctx.alignment_scheme = namespace + elif len(schemes_found) == 0: + return self.exit_codes.ERROR_INPUT_NO_SCHEME + else: + return self.exit_codes.ERROR_INPUT_BAD_SCHEME + + # Collect the inputs from the selected scheme + inputs = AttributeDict( + self.exposed_inputs(valid_schemes[self.ctx.alignment_scheme], + namespace=self.ctx.alignment_scheme)) + self.ctx.inputs = inputs + + + # Array should have the same shape - if not they should be interpolated to have the same shape + # Collect the arrays + # arrays = { + # 'first_potential': inputs.first_potential, + # 'second_potential': inputs.second_potential + # } + arrays = { + 'first_potential': self.inputs[self.ctx.alignment_scheme]['first_potential'], + 'second_potential': self.inputs[self.ctx.alignment_scheme]['second_potential'] + } + if 'charge_density' in inputs: # density-weighted case + arrays['charge_density'] = inputs.charge_density + + # # Check if ArrayData objects have more than one array packed in them + # for array in arrays.values(): + # if len(array.get_arraynames()) != 1: + # return self.exit_codes.ERROR_INPUT_EXTRA_ARRAYS + + # Unpack and obtain the shapes + array_shapes = {} + for array_name, array in arrays.items(): + shape = array.get_shape(array.get_arraynames()[0]) + array_shapes[array_name] = shape + + # Check if the shapes are the same. If not, we must be allowed to interpolate + self.ctx.interpolation_required = False + if len(set(array_shapes.values())) != 1: + self.ctx.interpolation_required = True + if not self.inputs.allow_interpolation: + return self.exit_codes.ERROR_INPUT_WRONG_SHAPE + + # For interpolation to be meaningful, the dimensions of the arrays must be compatible + # For example, if one grid was (3,1) and another was (1,3), how would interpolation + # be done? We try to avoid the situation where data is thrown away, and also one + # where we make new grids which are the product of others. + # Check that, when in ascending order according the dimension of the first axis, + # all other axis keep the correct ordering. + # If the reasoning was compelling, this could be relaxed later to the product type + # situation where having a (3,1) and a (1,3) would result in a target grid of (3,3) + + sorted_shapes = sorted(list(array_shapes.values())) + for index, shape in enumerate(sorted_shapes): + for axis in [1,2]: # Sorting is correct for axis 0, now check if the others are okay + if index == 0: + continue + else: + if (shape[axis] < sorted_shapes[index-1][axis]) : # Are the values not ascending? + return self.exit_codes.ERROR_INPUT_WRONG_ASPECT_RATIO + + # Keep the dicts for later use + self.ctx.arrays = arrays + self.ctx.array_shapes = array_shapes + + + def interpolation_required(self): + """ + Return wether interpolation of the input arrays is needed due to their sizes being mismatched + """ + return self.ctx.interpolation_required - self.ctx.alignment = 0.0 def do_interpolation(self): - """ + """ If interpolation is required, apply it """ - - if self.ctx.interpolation_required: - self.report('Doing potential alignment') - # TODO: Call the interpolation function and update the context + self.report('Interpolating between mismatched arrays') + + shapes_array = np.array(list(self.ctx.array_shapes.values())) + # Get max size along each axis - this is the target array size + target_shape = orm.List(list=[ + np.max(shapes_array[:,0]), + np.max(shapes_array[:,1]), + np.max(shapes_array[:,2]) + ]) + + self.report('Target interpolation size: {}'.format(target_shape.get_list())) + + self.report('Doing interpolation') + interpolated_arrays = {} + # for array_name, array in self.ctx.arrays.items(): + # interpolated_arrays[array_name] = get_interpolation( + # input_array=array, + # target_shape=target_shape) + # # Replace input arrays with interpolated versions + # for array_name, array in interpolated_arrays.items(): + # self.ctx.inputs[array_name] = array + + interpolated_arrays['first_potential'] = get_interpolation( + input_array=self.inputs[self.ctx.alignment_scheme]['first_potential'], + target_shape=target_shape) + interpolated_arrays['second_potential'] = get_interpolation( + input_array=self.inputs[self.ctx.alignment_scheme]['second_potential'], + target_shape=target_shape) + # Replace input arrays with interpolated versions + for array_name, array in interpolated_arrays.items(): + self.ctx.inputs[array_name] = array return @@ -102,19 +205,37 @@ def calculate_alignment(self): """ Calculate the alignment according to the requested scheme """ - # Call the correct alignment scheme - if self.inputs.alignment_scheme == 'lany-zunger': - # TODO: import and call the alignment function - self.ctx.alignment = testing() - #self.ctx.alignment = + # Get the correct alignment scheme workchain + alignment_workchain = valid_schemes[self.ctx.alignment_scheme] + + inputs = self.ctx.inputs + + workchain_future = self.submit(alignment_workchain, **inputs) + self.to_context(**{'alignment_wc': workchain_future}) return + + def check_alignment_workchain(self): + """ + Check if the model potential alignment workchain have finished correctly. + If yes, assign the outputs to the context + """ + + alignment_workchain = self.ctx['alignment_wc'] + + if not alignment_workchain.is_finished_ok: + self.report( + 'Potential alignment workchain has failed with status {}' + .format(alignment_workchain.exit_status)) + return self.exit_codes.ERROR_SUB_PROCESS_FAILED_ALIGNMENT + else: + self.ctx.alignment = alignment_workchain.outputs.alignment_required + def results(self): """ Collect results """ self.report( - "Completed alignment. An alignment of {} eV is required".format( - self.ctx.alignment.value)) + "Completed alignment. An alignment of {} eV is required".format(self.ctx.alignment.value)) self.out('alignment_required', self.ctx.alignment) diff --git a/aiida_defects/formation_energy/potential_alignment/utils.py b/aiida_defects/formation_energy/potential_alignment/utils.py index 34c3ba6..24de95e 100644 --- a/aiida_defects/formation_energy/potential_alignment/utils.py +++ b/aiida_defects/formation_energy/potential_alignment/utils.py @@ -6,6 +6,9 @@ # For further information on the license, see the LICENSE.txt file # ######################################################################################## from __future__ import absolute_import + +import numpy as np + from aiida.engine import calcfunction from aiida import orm """ @@ -16,7 +19,7 @@ @calcfunction def get_potential_difference(first_potential, second_potential): """ - Calculate the difference of two potentials + Calculate the difference of two potentials that have the same size Parameters ---------- @@ -35,9 +38,55 @@ def get_potential_difference(first_potential, second_potential): first_potential.get_arraynames()[0]) second_array = second_potential.get_array( second_potential.get_arraynames()[0]) + +# if first_array.shape != second_array.shape: +# target_shape = orm.List(list=np.max(np.vstack((first_array.shape, second_array.shape)), axis=0).tolist()) +# first_array = get_interpolation(first_potential, target_shape).get_array('interpolated_array') +# second_array = get_interpolation(second_potential, target_shape).get_array('interpolated_array') difference_array = first_array - second_array difference_potential = orm.ArrayData() difference_potential.set_array('difference_potential', difference_array) return difference_potential + +@calcfunction +def get_interpolation(input_array, target_shape): + """ + Interpolate an array into a larger array of size, `target_size` + + Parameters + ---------- + array: orm.ArrayData + Array to interpolate + target_shape: orm.List + The target shape to interpolate the array to + + Returns + ------- + interpolated_array + The calculated difference of the two potentials + """ + + from scipy.ndimage.interpolation import map_coordinates + + # Unpack + array = input_array.get_array(input_array.get_arraynames()[0]) + target_shape = target_shape.get_list() + + # map_coordinates takes two parameters - an input array and a coordinates + # array. It then evaluates what the interpolation of the input array should be + # at the target coordinates. + # Generate the target grid. + i = np.linspace(0, array.shape[0]-1, target_shape[0]) + j = np.linspace(0, array.shape[1]-1, target_shape[1]) + k = np.linspace(0, array.shape[2]-1, target_shape[2]) + ii,jj,kk = np.meshgrid(i,j,k, indexing='ij') + target_coords = np.array([ii,jj,kk]) + # Do the interpolation + interp_array = map_coordinates(input=np.real(array), coordinates=target_coords) + + interpolated_array = orm.ArrayData() + interpolated_array.set_array('interpolated_array', interp_array) + + return interpolated_array diff --git a/aiida_defects/formation_energy/utils.py b/aiida_defects/formation_energy/utils.py index 2c370d0..8e30733 100644 --- a/aiida_defects/formation_energy/utils.py +++ b/aiida_defects/formation_energy/utils.py @@ -7,7 +7,201 @@ ######################################################################################## from __future__ import absolute_import +from aiida import orm from aiida.engine import calcfunction +import numpy as np +import pymatgen +from pymatgen.core.composition import Composition +from pymatgen.core.sites import PeriodicSite +from pymatgen.core.periodic_table import Element +from pymatgen.core.structure import Structure + +def generate_defect_structure(host, site_coord, species): + ''' + To create defective structure at the site_coord in the host structure. species specify the type of defect to be created. + ''' + structure = host.get_pymatgen_structure() + defect_structure = structure.copy() + for atom, sign in species.items(): + if sign == 1: + defect_structure.append(atom, site_coord) + else: + site_index = find_index_of_site(structure, site_coord) + if site_index == None: + print('WARNING! the index of the defect site cannot be found') + defect_structure.remove_sites([site_index]) +# defect_structure.to(filename='tempo.cif') +# defect_structure = Structure.from_file('tempo.cif') + return orm.StructureData(pymatgen=defect_structure) + +def find_index_of_site(structure, site_coord): + #structure = host.get_pymatgen_structure() + lattice = structure.lattice + defect_site = PeriodicSite(Element('Li'), site_coord, lattice) # Li is just a dummy element. Any other element also works + for i, site in enumerate(structure): + if defect_site.distance(site) < 5E-4: + return i + +def get_vbm(calc_node): + #N_electron = calc_node.res.number_of_electrons + N_electron = calc_node.outputs.output_parameters.get_dict()['number_of_electrons'] + vb_index = int(N_electron/2)-1 + vbm = np.amax(calc_node.outputs.output_band.get_array('bands')[:,vb_index]) + + return vbm + +def is_intrinsic_defect(species, compound): + """ + Check if a defect is an intrisic or extrinsic defect + """ + composition = Composition(compound) + element_list = [atom.symbol for atom in composition] + + for atom in species.keys(): + if atom not in element_list: + return False + return True + +def get_dopant(species, compound): + """ + Get the dopant + """ + composition = Composition(compound) + element_list = [atom.symbol for atom in composition] + for atom in species.keys(): + if atom not in element_list: + return atom + return 'intrinsic' + +def get_defect_and_charge_from_label(calc_label): + spl = calc_label.split('[') + defect = spl[0] + chg = float(spl[1].split(']')[0]) + return defect, chg + +@calcfunction +def get_data_array(array): + data_array = array.get_array('data') + v_data = orm.ArrayData() + v_data.set_array('data', data_array) + return v_data + +@calcfunction +def get_defect_formation_energy(defect_data, E_Fermi, chem_potentials, pot_alignments): + ''' + Computing the defect formation energy with and without electrostatic and potential alignment corrections + Note: 'E_corr' in the defect_data corresponds to the total correction, i.e electrostatic and potential alignment + ''' + defect_data = defect_data.get_dict() + E_Fermi = E_Fermi.get_array('data') + chem_potentials = chem_potentials.get_dict() + pot_alignments = pot_alignments.get_dict() + + E_defect_formation = {'uncorrected':{}, 'electrostatic': {}, 'electrostatic and alignment': {}} + for defect, properties in defect_data.items(): + E_defect_formation['uncorrected'][defect] = {} + E_defect_formation['electrostatic'][defect] = {} + E_defect_formation['electrostatic and alignment'][defect] = {} + + for chg in properties['charges'].keys(): + Ef_raw = properties['charges'][chg]['E']-properties['E_host']+float(chg)*(E_Fermi+properties['vbm']) + for spc, sign in properties['species'].items(): + Ef_raw -= sign*chem_potentials[spc][0] + Ef_corrected = Ef_raw + properties['charges'][chg]['E_corr'] + + E_defect_formation['uncorrected'][defect][str(chg)] = Ef_raw + E_defect_formation['electrostatic'][defect][str(chg)] = Ef_corrected + float(chg)*pot_alignments[defect][str(chg)] + E_defect_formation['electrostatic and alignment'][defect][str(chg)] = Ef_corrected + + return orm.Dict(dict=E_defect_formation) + +# @calcfunction +# def get_defect_formation_energy(defect_data, E_Fermi, pot_alignments, chem_potentials, compound): + +# defect_data = defect_data.get_dict() +# #formation_energy_dict = formation_energy_dict.get_dict() +# E_Fermi = E_Fermi.get_dict() +# chem_potentials = chem_potentials.get_dict() +# pot_alignments = pot_alignments.get_dict() +# compound = compound.value + +# intrinsic_defects = {} +# for defect, properties in defect_data.items(): +# if is_intrinsic_defect(properties['species'], compound): +# intrinsic_defects[defect] = properties + +# defect_Ef = {} +# for dopant, e_fermi in E_Fermi.items(): +# defect_temp = intrinsic_defects.copy() +# if dopant != 'intrinsic': +# for defect, properties in defect_data.items(): +# if dopant in properties['species'].keys(): +# defect_temp[defect] = properties + +# defect_Ef[dopant] = defect_formation_energy( +# defect_temp, +# e_fermi, +# chem_potentials[dopant], +# pot_alignments +# ) + +# return orm.Dict(dict=defect_Ef) + +def has_numbers(inputString): + return any(char.isdigit() for char in inputString) + +def convert_key(key): + new_key = key.replace('-', 'q') + if has_numbers(key): + new_key = 'A'+new_key + return new_key.replace('.', '_') + else: + return new_key + +def revert_key(key): + new_key = key.replace('q', '-') + if has_numbers(key): + return new_key[1:]#.replace('_', '.') + else: + return new_key + +@calcfunction +def store_dict(**kwargs): + new_dict = {} + for k, v in kwargs.items(): + new_k = revert_key(k) + if isinstance(v, orm.Dict): + # new_dict[k.replace('q', '-')] = v.get_dict() + d = {key.replace('_', '.'): item for key, item in v.get_dict().items()} + new_dict[new_k] = d + if isinstance(v, orm.Float): + new_dict[new_k] = v.value + if isinstance(v, orm.ArrayData): + new_dict[new_k] = v.get_array(v.get_arraynames()[0]).item() # get the value from 0-d numpy array + return orm.Dict(dict=new_dict) + + +@calcfunction +def get_defect_data(dopant, compound, defect_info, vbm, E_host_outputs_params, total_correction, **kwargs): + + dopant = dopant.value + compound = compound.value + vbm = vbm.value + E_host = E_host_outputs_params.get_dict()['energy'] + defect_info = defect_info.get_dict() + total_correction = total_correction.get_dict() + + defect_data = {} + for defect, properties in defect_info.items(): + if is_intrinsic_defect(properties['species'], compound) or dopant in properties['species'].keys(): + defect_data[defect] = {'N_site': properties['N_site'], 'species': properties['species'], 'charges': {}, + 'vbm': vbm, 'E_host': E_host} + for chg in properties['charges']: + defect_data[defect]['charges'][str(chg)] = {'E_corr': total_correction[defect][str(chg)], + 'E': kwargs[convert_key(defect)+'_'+convert_key(str(chg))].get_dict()['energy'] + } + + return orm.Dict(dict=defect_data) def run_pw_calculation(pw_inputs, structure, charge): @@ -45,14 +239,18 @@ def run_pw_calculation(pw_inputs, structure, charge): @calcfunction -def get_raw_formation_energy(defect_energy, host_energy, chemical_potential, +def get_raw_formation_energy(defect_energy, host_energy, chempot_sign, chemical_potential, charge, fermi_energy, valence_band_maximum): """ Compute the formation energy without correction """ - e_f_uncorrected = defect_energy - host_energy - chemical_potential + ( - charge * (valence_band_maximum + fermi_energy)) - return e_f_uncorrected + chempot_sign = chempot_sign.get_dict() + chemical_potential = chemical_potential.get_dict() + + e_f_uncorrected = defect_energy.value - host_energy.value + charge.value*(valence_band_maximum.value + fermi_energy.value) + for specie, sign in chempot_sign.items(): + e_f_uncorrected -= sign*chemical_potential[specie] + return orm.Float(e_f_uncorrected) @calcfunction @@ -63,13 +261,12 @@ def get_corrected_formation_energy(e_f_uncorrected, correction): e_f_corrected = e_f_uncorrected + correction return e_f_corrected - @calcfunction -def get_corrected_aligned_formation_energy(e_f_corrected, alignment): +def get_corrected_aligned_formation_energy(e_f_corrected, defect_charge, alignment): """ Compute the formation energy with correction and aligned """ - e_f_corrected_aligned = e_f_corrected + alignment + e_f_corrected_aligned = e_f_corrected - defect_charge * alignment return e_f_corrected_aligned diff --git a/aiida_defects/tests/__init__.py b/aiida_defects/tests/__init__.py deleted file mode 100644 index 3303208..0000000 --- a/aiida_defects/tests/__init__.py +++ /dev/null @@ -1,185 +0,0 @@ -""" tests for the plugin - -Use the aiida.utils.fixtures.PluginTestCase class for convenient -testing that does not pollute your profiles/databases. -""" - -# Helper functions for tests -from __future__ import absolute_import -from __future__ import print_function -import os -import tempfile -import aiida_diff.utils as utils - -TEST_DIR = os.path.dirname(os.path.realpath(__file__)) -TEST_COMPUTER = 'localhost-test' - -executables = { - 'diff': 'diff', -} - - -def get_path_to_executable(executable): - """ Get path to local executable. - - :param executable: Name of executable in the $PATH variable - :type executable: str - - :return: path to executable - :rtype: str - """ - # pylint issue https://github.com/ConradJohnston/aiida-defectsQA/pylint/issues/73 - import distutils.spawn # pylint: disable=no-name-in-module,import-error - path = distutils.spawn.find_executable(executable) - if path is None: - raise ValueError("{} executable not found in PATH.".format(executable)) - - return path - - -def get_computer(name=TEST_COMPUTER, workdir=None): - """Get AiiDA computer. - - Loads computer 'name' from the database, if exists. - Sets up local computer 'name', if it isn't found in the DB. - - :param name: Name of computer to load or set up. - :param workdir: path to work directory - Used only when creating a new computer. - - :return: The computer node - :rtype: :py:class:`aiida.orm.Computer` - """ - from aiida.orm import Computer - from aiida.common.exceptions import NotExistent - - if utils.AIIDA_VERSION < utils.StrictVersion('1.0a0'): - try: - computer = Computer.get(name) - except NotExistent: - # pylint: disable=abstract-class-instantiated,no-value-for-parameter, unexpected-keyword-arg - if workdir is None: - workdir = tempfile.mkdtemp() - - computer = Computer( - name=name, - description='localhost computer set up by aiida_diff tests', - hostname=name, - workdir=workdir, - transport_type='local', - scheduler_type='direct', - enabled_state=True) - #TODO: simpify once API improvements are in place - else: - from aiida.orm.backend import construct_backend - backend = construct_backend() - - try: - computer = backend.computers.get(name=name) - except NotExistent: - if workdir is None: - workdir = tempfile.mkdtemp() - - computer = backend.computers.create( - name=name, - description='localhost computer set up by aiida_diff tests', - hostname=name, - workdir=workdir, - transport_type='local', - scheduler_type='direct', - enabled_state=True) - - computer.store() - - # TODO configure computer for user, see - # aiida_core.aiida.cmdline.commands.computer.Computer.computer_configure - - return computer - - -def get_code(entry_point, computer=None): - """Get local code. - - Sets up code for given entry point on given computer. - - :param entry_point: Entry point of calculation plugin - :param computer_name: Name of (local) computer - - :return: The code node - :rtype: :py:class:`aiida.orm.Code` - """ - from aiida.orm import Code - from aiida.common.exceptions import NotExistent - - try: - executable = executables[entry_point] - except KeyError: - raise KeyError( - "Entry point {} not recognized. Allowed values: {}".format( - entry_point, list(executables.keys()))) - - if computer is None: - computer = get_computer() - - try: - code = Code.get_from_string('{}@{}'.format(executable, - computer.get_name())) - except NotExistent: - path = get_path_to_executable(executable) - code = Code( - input_plugin_name=entry_point, - remote_computer_exec=[computer, path], - ) - code.label = executable - code.store() - - return code - - -def test_calculation_execution(calc, - allowed_returncodes=(0, ), - check_paths=None): - """ test that a calculation executes successfully - - :param calc: the calculation - :param allowed_returncodes: raise RunTimeError if return code is not in allowed_returncodes - :param check_paths: raise OSError if these relative paths are not in the folder after execution - :return: - """ - # pylint: disable=too-many-locals - from aiida.common.folders import SandboxFolder - import stat - import subprocess - - # output input files and scripts to temporary folder - with SandboxFolder() as folder: - - subfolder, script_filename = calc.submit_test(folder=folder) - print("inputs created at {}".format(subfolder.abspath)) - - script_path = os.path.join(subfolder.abspath, script_filename) - scheduler_stderr = calc._SCHED_ERROR_FILE # pylint: disable=protected-access - - # we first need to make sure the script is executable - st = os.stat(script_path) - os.chmod(script_path, st.st_mode | stat.S_IEXEC) - # now call script, NB: bash -l -c is required to access global variable loaded in .bash_profile - returncode = subprocess.call(["bash", "-l", "-c", script_path], - cwd=subfolder.abspath) - - if returncode not in allowed_returncodes: - - err_msg = "process failed (and couldn't find stderr file: {})".format( - scheduler_stderr) - stderr_path = os.path.join(subfolder.abspath, scheduler_stderr) - if os.path.exists(stderr_path): - with open(stderr_path) as f: - err_msg = "Process failed with stderr:\n{}".format( - f.read()) - raise RuntimeError(err_msg) - - if check_paths is not None: - for outpath in check_paths: - subfolder.get_abs_path(outpath, check_existence=True) - - print("calculation completed execution") diff --git a/aiida_defects/utils.py b/aiida_defects/utils.py new file mode 100644 index 0000000..9a73314 --- /dev/null +++ b/aiida_defects/utils.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +import numpy as np + +from qe_tools import CONSTANTS + +# This a collection of common, generic methods for common tasks + +def get_cell_matrix(structure): + """ + Get the cell matrix (in bohr) from an AiiDA StructureData object + + Parameters + ---------- + structure: AiiDA StructureData + The structure object of interest + + Returns + ------- + cell_matrix + 3x3 cell matrix array in units of Bohr + + """ + cell_matrix = np.array(structure.cell) / CONSTANTS.bohr_to_ang # Angstrom to Bohr + return cell_matrix + + +def get_reciprocal_cell(cell_matrix): + """ + For a given cell_matrix, compute the reciprocal cell matrix + + Parameters + ---------- + cell_matrix: 3x3 array + Cell matrix of the real space cell + + Returns + ------- + reciprocal_cell + 3x3 cell matrix array in reciprocal units + """ + from numpy.linalg import inv + reciprocal_cell = (2 * np.pi * inv(cell_matrix)).transpose() # Alternative definition (2pi) + + return reciprocal_cell + +def calc_pair_distance_xyz(cellmat,ri,rj): + """" + Calculate the distance between two atoms accross periodic boundary conditions + starting from cartesian coords. + Uses the general algorithm for the minimum image (Appendix B - Eq 9.) from: + M. E. Tuckerman. Statistical Mechanics: Theory and Molecular Simulation. + Oxford University Press, Oxford, UK, 2010. + + Parameters + ---------- + cellmat_inv - 3x3 matrix + The inverse of the 3x3 matrix describing the simulation cell + ri,rj - 3x1 vector + numpy vectors describing the position of atoms i and j + + Returns + --------- + dist - float + The distance between the atoms i and j, according the minimum image + convention. + + """ + si=np.dot(cellmat_inv,ri) + sj=np.dot(cellmat_inv,rj) + sij=si-sj + sij=sij-np.rint(sij) + rij=np.dot(cellmat,sij) + + # Get the magnitude of the vector + dist=np.sqrt(np.dot(rij,rij)) + + return dist + +def get_grid(dimensions, endpoint=True): + """ + Generate an array of coordinates + """ + # Generate a grid of coordinates + i = np.linspace(0., 1., dimensions[0], endpoint) + j = np.linspace(0., 1., dimensions[1], endpoint) + k = np.linspace(0., 1., dimensions[2], endpoint) + # Generate NxN arrays of coords + iii, jjj, kkk = np.meshgrid(i, j, k, indexing='ij') + # Flatten this to a 3xNN array + ijk_array = np.array([iii.ravel(), jjj.ravel(), kkk.ravel()]) + + return ijk_array + +def get_xyz_coords(cell_matrix, dimensions): + """ + For a given array, generate an array of xyz coordinates in the cartesian basis + """ + ijk_array = get_grid(dimensions) + # Change the crystal basis to a cartesian basis + xyz_array = np.dot(cell_matrix.T, ijk_array) + + return xyz_array \ No newline at end of file diff --git a/examples/ChemicalPotential.ipynb b/examples/ChemicalPotential.ipynb new file mode 100644 index 0000000..ee3d292 --- /dev/null +++ b/examples/ChemicalPotential.ipynb @@ -0,0 +1,125 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "25dabefe", + "metadata": {}, + "source": [ + "# How to use the ChemicalPotential Workchain\n", + "To compute the formation energy of a defect, one needs to know the chemical potential of the element that is added or removed to create that defect. That chemical potential has to be chosen in such a way that it is compatible with the stability of the host structure with respect to the other phases in the phase diagram. In the example below, we show you how to compute the stability region of a compound Li$_3$PO$_4$ using the `ChemicalPotential` workchain that is part of the `AiiDA-defects` package. Once the stability region is determined, the chemical potential can be chosen from this region and use in the calculation of the defect formation energy. By default, the chemical potential of the centroid of the stability region is chosen." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6cfda92", + "metadata": {}, + "outputs": [], + "source": [ + "# Get your normal profile\n", + "%load_ext aiida" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fceca655", + "metadata": {}, + "outputs": [], + "source": [ + "%aiida" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "691a2e4c", + "metadata": {}, + "outputs": [], + "source": [ + "#Import the workchain and common aiida functionalities\n", + "from aiida import orm, engine\n", + "from aiida_defects.formation_energy.chemical_potential.chemical_potential import ChemicalPotentialWorkchain" + ] + }, + { + "cell_type": "markdown", + "id": "f356b1bd", + "metadata": {}, + "source": [ + "**Explanation of inputs parameters**\n", + "\n", + "'formation_energy_dict': formation energies of all stable compounds in the given phase diagram. The keys are the name of the compounds and the values are their formation energy (per formula unit). These numbers can be taken from your favorite material databases or computed on your own. In anycase, you have to make sure that they are computed using the same DFT setup (k-point mesh, xcf functionals, planewave cutoff,...) as the one you used for calculation of the supercell energy (with and without defects)\n", + "'compound': the name of the host compound you are studying.\n", + "'dependent_element': Element whose chemical potential is determined once the chemical potentials of the elements are fixed. In our case, we chose P as the dependent element but it can also be Li or O.\n", + "'ref_energy': Energy of the element in its standar state. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f781c1b4", + "metadata": {}, + "outputs": [], + "source": [ + "Ef_dict = {'Li3PO4': -22.0891, 'LiP': -1.0465, 'LiP7': -1.2718, 'Li3P7': -3.5958, 'Li3P': -2.7859, 'LiO8': -3.6499, 'Li2O2': -6.6031,\n", + " 'Li2O': -6.2001, 'P2O5': -17.1485, 'Li4P2O7': -35.7771, 'LiPO3': -13.5973}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9eca0a27", + "metadata": {}, + "outputs": [], + "source": [ + "inputs = {\n", + " 'formation_energy_dict' : orm.Dict(dict=Ef_dict),\n", + " 'compound' : orm.Str('Li3PO4'),\n", + " 'dependent_element' : orm.Str('P'),\n", + " 'ref_energy' : orm.Dict(dict={'Li':-195.5141, 'P':-191.0388, 'O':-557.4985})\n", + " }\n", + "workchain_future, pk = engine.run_get_pk(ChemicalPotentialWorkchain, **inputs)" + ] + }, + { + "cell_type": "markdown", + "id": "2e812e74", + "metadata": {}, + "source": [ + "**Optional parameters**\n", + "\n", + "If you want to study the effect of dopant concentration in the so-called 'frozen-defect' approach, you have to specify that frozen defect in the inputs for ex., 'dopant_elements' : orm.List(list=['O'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0343d3bf", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/DefectChemistry.ipynb b/examples/DefectChemistry.ipynb new file mode 100644 index 0000000..c59b5f1 --- /dev/null +++ b/examples/DefectChemistry.ipynb @@ -0,0 +1,282 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3ea5e76f", + "metadata": {}, + "source": [ + "# How to use the DefectChemistry Workchain\n", + "\n", + "This notebook will explain the use of the `DefectChemistry` workchain.\n", + "\n", + "In other to select suitable dopants for a materials, one has to consider several dopants possibly in various charge states. `DefectChemistry` workchain is the top most workchain in `AiiDA-defects` package that was designed to make this calculation seamless and easily reproducible. \n", + "Once all the necessary inputs are provided, the workchain will compute all the necessary terms including the chemical potential and the Fermi level in a consistent way without the need for human intervention.\n", + "An example of the input file is shown below:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e46c870", + "metadata": {}, + "outputs": [], + "source": [ + "# Get your normal profile\n", + "%load_ext aiida" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be681a85", + "metadata": {}, + "outputs": [], + "source": [ + "%aiida" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd8deee5", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from aiida import orm, engine, common\n", + "\n", + "#Import the workchain \n", + "from aiida_defects.formation_energy.defect_chemistry_qe import DefectChemistryWorkchainQE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "679c104a", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up structures\n", + "from pymatgen.core.structure import Structure\n", + "\n", + "pymatgen_structure = Structure.from_file(\"./Structures/Li3ClO_1x1x1.cif\")\n", + "\n", + "# Unitcell\n", + "unitcell_structure = orm.StructureData(pymatgen=pymatgen_structure)\n", + "\n", + "# Host 2x2x2 supercell\n", + "pymatgen_structure.make_supercell([2,2,2])\n", + "host_structure = orm.StructureData(pymatgen=pymatgen_structure)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "613f53e4", + "metadata": {}, + "outputs": [], + "source": [ + "# List all possible defects you want to consider when determining the defect chemistry of your material\n", + "defect_dict = {\n", + " 'V_Li': {'N_site': 3, 'species': {'Li':-1}, 'defect_position': [0.25, 0.0, 0.0], 'charges': [-1.0]},\n", + " 'V_O': {'N_site': 1, 'species': {'O':-1}, 'defect_position': [0.0, 0.0, 0.0], 'charges': [2.0]},\n", + " 'V_Cl': {'N_site': 1,'species': {'Cl':-1}, 'defect_position': np.array([0.25, 0.25, 0.25]), 'charges': np.array([2.0])},\n", + " 'N-O': {'N_site': 1,'species': {'N':1, 'O':-1}, 'defect_position': [0.0, 0.0, 0.0], 'charges': [-1.0]},\n", + " 'Mg-Li': {'N_site': 3, 'species': {'Mg':1, 'Li':-1}, 'defect_position': [0.25, 0.0, 0.0], 'charges': [1.0]},\n", + " }\n", + "\n", + "# Formation energies required by the ChemicalPotential workchain\n", + "Ef_dict = {\n", + " 'intrinsic': {'Li3ClO': -11.075, 'LiClO4': -6.7039, 'LiCl': -4.2545, 'ClO2': -1.6839, 'Cl2O7': -4.0196, 'ClO3': -1.9974, 'Cl2O': -1.1942, \n", + " 'LiO8': -3.6499, 'Li2O': -6.2001, 'Li2O2': -6.6031},\n", + " 'N': {'Li3ClO': -11.075, 'LiClO4': -6.7039, 'LiCl': -4.2545, 'Li4NCl': -6.1485, 'NCl3': -0.2089, 'NClO3': -3.6343, 'NClO6': -4.5814, \n", + " 'NClO': -1.5903, 'ClO2': -1.6839, 'Cl2O7': -4.0196, 'ClO3': -1.9974, 'Cl2O': -1.1942, 'LiN3': -1.6777, 'Li3N': -1.8502, 'LiNO3': -7.3655,\n", + " 'LiO8': -3.6499, 'Li2O': -6.2001, 'Li2O2': -6.6031, 'N2O5': -5.1059, 'N2O': -1.3466, 'NO2': -2.4734},\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "339296f4", + "metadata": {}, + "outputs": [], + "source": [ + "# Codes\n", + "pw_code = orm.Code.get_from_string(\"pw-7.0-qe-eiger\")\n", + "\n", + "# Pseudos\n", + "pseudo_family = orm.Str('SSSP/1.2/PBEsol/efficiency') # This is the label that was used when installing the pseudos" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16a7e0db", + "metadata": {}, + "outputs": [], + "source": [ + "# If known, the dielctric constant can be directly provided as below. If not, it can be computed within the workchain by specifying the ph code\n", + "dielectric = orm.ArrayData()\n", + "dielectric.set_array('epsilon', np.array([[3.12, 0., 0.,], [0., 3.12, 0.], [0., 0., 3.12]]))\n", + "\n", + "# Covariance matrix is needed to construct the gaussian charge model for the correction workchain.\n", + "cov_matrix = orm.ArrayData()\n", + "cov_matrix.set_array('sigma', np.eye(3))\n", + "\n", + "inputs = {\n", + " 'restart_wc' : orm.Bool(False),\n", + " # 'restart_node' : orm.Int(29729), \n", + " # Structures\n", + " 'unitcell' : unitcell_structure,\n", + " 'host_structure' : host_structure,\n", + " 'defect_info' : orm.Dict(dict=defect_dict),\n", + " # Chemical potential\n", + " 'formation_energy_dict' : orm.Dict(dict=Ef_dict),\n", + " 'compound' : orm.Str('Li3ClO'),\n", + " 'dependent_element' : orm.Str('Li'),\n", + " 'ref_energy' : orm.Dict(dict={'Li':-195.51408,'O':-557.49850,'Cl':-451.66500, 'N':-274.00734, 'Mg':-445.18254}),\n", + " 'temperature' : orm.Float(300.0), # in Kelvin\n", + " # Correction scheme\n", + " 'correction_scheme' : orm.Str('gaussian'),\n", + " 'epsilon' : dielectric, # epsilon_inf = 3.2\n", + " 'cutoff' : orm.Float(100.0),\n", + " 'charge_model': {\n", + " 'model_type': orm.Str('fixed'),\n", + " 'fixed':{\n", + " 'covariance_matrix': cov_matrix}\n", + " # 'fitted': {\n", + " # 'tolerance': orm.Float(1.0e-3),\n", + " # 'strict_fit': orm.Bool(True),\n", + " # }\n", + " },\n", + " }\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbaf384a", + "metadata": {}, + "outputs": [], + "source": [ + "# Scheduler options\n", + "pw_metadata = orm.Dict(dict={ \n", + " 'options': {\n", + " 'max_wallclock_seconds': 1*60*60, \n", + " 'resources': {\n", + " 'num_machines': 1,\n", + " 'num_mpiprocs_per_machine': 64, \n", + " 'num_cores_per_mpiproc': 1, \n", + " }\n", + " }\n", + " })\n", + "pw_settings = orm.Dict(dict={'cmdline': ['-nk', '4']})\n", + "\n", + "pw_metadata_unitcell = orm.Dict(dict={\n", + " 'options': {\n", + " 'max_wallclock_seconds': 1*60*60, \n", + " 'resources': {\n", + " 'num_machines': 1,\n", + " 'num_mpiprocs_per_machine': 16,\n", + " 'num_cores_per_mpiproc': 1,\n", + " }\n", + " }\n", + " })\n", + "pw_settings_unitcell = orm.Dict(dict={'cmdline': ['-nk', '2']})\n", + "\n", + "dos_code = orm.Code.get_from_string('dos-7.0-qe-eiger')\n", + "dos_metadata = orm.Dict( dict={\n", + " 'options': {\n", + " 'max_wallclock_seconds': 10*60,\n", + " 'resources': {\n", + " 'num_machines': 1,\n", + " 'num_mpiprocs_per_machine': 16,\n", + " 'num_cores_per_mpiproc': 1,\n", + " }\n", + " },\n", + "})\n", + "\n", + "pp_code = orm.Code.get_from_string('pp-7.0-qe-eiger')\n", + "pp_metadata = orm.Dict( dict={\n", + "# 'description': 'Li3ClO',\n", + " 'options': {\n", + " 'max_wallclock_seconds': 10*60,\n", + " 'resources': {\n", + " 'num_machines': 1,\n", + " 'num_mpiprocs_per_machine': 64,\n", + " 'num_cores_per_mpiproc': 1, \n", + " }\n", + " },\n", + "})\n", + "\n", + "# Computational (chosen code is QE)\n", + "inputs['qe'] = {\n", + " 'dft': {\n", + " 'supercell' : {\n", + " 'relaxation_scheme': orm.Str('fixed'),\n", + " 'code' : pw_code,\n", + " #'kpoints': kpoints,\n", + " 'pseudopotential_family': pseudo_family,\n", + " 'parameters' : pw_parameters,\n", + " 'scheduler_options' : pw_metadata,\n", + " 'settings' : pw_settings,\n", + " },\n", + " 'unitcell' : {\n", + " 'code' : pw_code,\n", + " #'kpoints': kpoints_unitcell,\n", + " 'pseudopotential_family': pseudo_family,\n", + " 'parameters' : pw_parameters,\n", + " 'scheduler_options' : pw_metadata_unitcell,\n", + " 'settings' : pw_settings_unitcell,\n", + " }\n", + " },\n", + " 'dos' : {\n", + " 'code' : dos_code,\n", + " 'scheduler_options' : dos_metadata,\n", + " },\n", + "# 'dfpt' : {\n", + "# 'code' : ph_code,\n", + "# 'scheduler_options' : ph_metadata,\n", + "# },\n", + " 'pp' : {\n", + " 'code' : pp_code,\n", + " 'scheduler_options' : pp_metadata,\n", + " }\n", + " }\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fb6db75", + "metadata": {}, + "outputs": [], + "source": [ + "workchain_future = engine.submit(DefectChemistryWorkchainQE, **inputs)\n", + "print('Submitted workchain with PK=' + str(workchain_future.pk))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/FormationEnergyQE.ipynb b/examples/FormationEnergyQE.ipynb index 246ded9..c375e78 100644 --- a/examples/FormationEnergyQE.ipynb +++ b/examples/FormationEnergyQE.ipynb @@ -15,20 +15,9 @@ "1. Run self-consistent DFT energy calculations for the host, neutral defect and charged defect supercells.\n", "2. Run PP.x to obtain the electrostatic potentials.\n", "3. Run a further DFT calculation on a unitcell (optionally).\n", - "4. Run DFPT to obtain the relative permitivitty.\n", + "4. Run DFPT to obtain the relative permitivitty (optionally).\n", "5. Run the `GaussianCountercharge` workchain to obtain the correction.\n", - "6. Compute the formation energy, with and without corrections.\n", - "\n", - "**NOTE!**\n", - "In this alpha version of `AiiDA-Defects` there are a number of features which are either not yet implemented or not well tested. These are listed below, with no guarentee of this being an exhaustive list.\n", - "Please bear these considerations in mind when testing the workchain.\n", - "\n", - "* The PP.x plugin used must be from [my AiiDA-QuantumESPRESSO fork](https://github.com/ConradJohnston/aiida-quantumespresso/tree/pp-parser) until this is merged into the official release\n", - "* Alignment of the electrostatic potentials is not yet automatic. Placeholder code will return 0.0 eV for these steps. The alignment should be done 'by hand'. A typical option is to take a planar average of the electrostatic potential along some axis, and then to align far from the defect. For cubic cells, a convenient option is to place point defects in the center of the box, and then take the alignment at the edge. \n", - "* In principle, only cubic cells are currently supported, although for a large-enough supercell, it shouldn't matter. This would be interesting to prove/disprove. A change to any shape of supercell is on the TODO list.\n", - "* The width of the model gaussian is fixed currently, with a TODO for a routine to fit this. If one wants to play with this for testing, I can expose it as an input.\n", - "\n", - "\n" + "6. Compute the formation energy, with and without corrections." ] }, { @@ -46,13 +35,27 @@ "outputs": [], "source": [ "# Get your normal profile\n", - "from aiida import load_profile\n", - "load_profile()\n", - "\n", + "%load_ext aiida" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%aiida" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "# Import commonly used functionality\n", "import numpy as np\n", - "from aiida import orm, engine, common\n", - "from aiida.plugins import WorkflowFactory" + "from aiida import orm, engine, common" ] }, { @@ -68,7 +71,8 @@ "metadata": {}, "outputs": [], "source": [ - "from aiida_defects.formation_energy.formation_energy_qe import FormationEnergyWorkchainQE" + "from aiida_defects.formation_energy.formation_energy_qe import FormationEnergyWorkchainQE\n", + "from aiida_defects.formation_energy.utils import generate_defect_structure" ] }, { @@ -85,9 +89,9 @@ "outputs": [], "source": [ "# Set up structures\n", - "import pymatgen\n", + "from pymatgen.core.structure import Structure\n", "\n", - "pymatgen_structure = pymatgen.Structure.from_file(\"./Structures/Diamond_1x1x1.cif\")\n", + "pymatgen_structure = Structure.from_file(\"./Structures/Li3ClO_1x1x1.cif\")\n", "\n", "# Unitcell\n", "unitcell_structure = orm.StructureData(pymatgen=pymatgen_structure)\n", @@ -96,9 +100,9 @@ "pymatgen_structure.make_supercell([2,2,2])\n", "host_structure = orm.StructureData(pymatgen=pymatgen_structure)\n", "\n", - "# Defect (Carbon vacancy) 2x2x2 supercell\n", - "pymatgen_structure.remove_sites(indices=[0])\n", - "defect_structure = orm.StructureData(pymatgen=pymatgen_structure)" + "# Defect (Lithium vacancy) 2x2x2 supercell\n", + "defect_position = [0.0, 0.0, 0.0]\n", + "defect_structure = generate_defect_structure(host_structure, defect_position, {'Li': -1}) # the value -1 means removing (to create vacancy) while +1 mean adding (to create interstitial)" ] }, { @@ -112,7 +116,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "4a. Set up the supercell calculations" + "4a. Set up the supercell calculations. Most of the parameters needed for the pw calculations are taken from the aiida-quantum espresso protocol but the users can also overwrite these default parameters by their owns." ] }, { @@ -121,70 +125,34 @@ "metadata": {}, "outputs": [], "source": [ - "# Set up a PW calculation\n", - "\n", - "# PW code\n", "pw_code = orm.Code.get_from_string('pw@localhost')\n", "\n", - "# Add the minimum parameters needed, plus any additional needed for the system of interest\n", - "pw_parameters = orm.Dict(dict={\n", - " 'CONTROL': {\n", - " 'calculation': 'scf',\n", - " 'restart_mode': 'from_scratch',\n", - " 'wf_collect': True,\n", - " },\n", - " 'SYSTEM': {\n", - " 'ecutwfc': 45,\n", - " 'ecutrho': 360.,\n", - " },\n", - " 'ELECTRONS': {\n", - " 'conv_thr': 1.e-7,\n", - " }})\n", + "# set this parameters to True if you start the calculations from scratch\n", + "run_pw = True\n", + "run_v = True\n", + "run_rho = True\n", "\n", - "# Setup k-points\n", - "kpoints = orm.KpointsData()\n", - "kpoints.set_kpoints_mesh([1,1,1]) # Definately not converged, but we want the example to run quickly\n", - "\n", - "# Psuedos \n", - "pseudo_family = 'SSSP' # This is the label that was used when installing the pseudos\n", - "pseudos = orm.nodes.data.upf.get_pseudos_from_structure(host_structure,pseudo_family)\n", + "# Pseudos \n", + "pseudo_family = orm.Str('SSSP/1.2/PBEsol/efficiency') # This is the label that was used when installing the pseudos\n", "\n", "# Scheduler options\n", "pw_metadata = orm.Dict( dict={\n", - " 'description': 'Diamond test', \n", + " 'description': 'Li3ClO test', \n", " 'options': {\n", " 'max_wallclock_seconds': 1800, \n", " 'resources': {\n", " 'num_machines': 1\n", " }\n", " }, \n", - " 'label': 'Diamond test'\n", - "})\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "4b. Set up the unitcell calculation. This is optional, but likely to be necessary to converge the relative permitivitty. Using a unitcell, but with a much denser k-mesh is the expected usage." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Set up the unitcell PW calculation \n", - "kpoints_unitcell = orm.KpointsData()\n", - "kpoints_unitcell.set_kpoints_mesh([20,20,20])" + " 'label': 'Li3ClO test'\n", + "})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "4c. Set up the post-processing calculations. These are used to obtain the electrostatic potentials from the supercell calculations." + "4b. Set up the post-processing calculations. These are used to obtain the electrostatic potentials from the supercell calculations." ] }, { @@ -197,46 +165,17 @@ "\n", "# Scheduler options\n", "pp_metadata = orm.Dict( dict={\n", - " 'description': 'Diamond test', \n", + " 'description': 'Li3ClO test', \n", " 'options': {\n", " 'max_wallclock_seconds': 1800, \n", " 'resources': {\n", " 'num_machines': 1\n", " }\n", " }, \n", - " 'label': 'Diamond test'\n", + " 'label': 'Li3ClO test'\n", "})\n" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "4d. Set up the post-processing calculations. These are used to obtain the electrostatic potentials from the supercell calculations." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ph_code = orm.Code.get_from_string('ph@localhost')\n", - "\n", - "\n", - "ph_metadata = orm.Dict( dict={\n", - " 'description': 'Diamond test', \n", - " 'options': {\n", - " 'max_wallclock_seconds': 1800, \n", - " 'resources': {\n", - " 'num_machines': 28\n", - " },\n", - " 'queue_name' : 'debug'\n", - " }, \n", - " 'label': 'Diamond test'\n", - "})" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -250,48 +189,81 @@ "metadata": {}, "outputs": [], "source": [ + "# If known, the dielctric constant can be directly provided as below. If not, it can be computed within the workchain by specifying the ph code\n", + "dielectric = orm.ArrayData()\n", + "dielectric.set_array('epsilon', np.array([[3.12, 0., 0.,], [0., 3.12, 0.], [0., 0., 3.12]]))\n", + "\n", + "# Covariance matrix is needed to construct the gaussian charge model for the correction workchain.\n", + "cov_matrix = orm.ArrayData()\n", + "cov_matrix.set_array('sigma', np.eye(3))\n", "inputs = {\n", + " 'relaxation_scheme': orm.Str('fixed'), # Run only scf calculation without relaxation\n", " # Structures\n", " 'host_structure': host_structure,\n", " 'defect_structure': defect_structure,\n", " 'host_unitcell' : unitcell_structure,\n", " # Defect information \n", - " 'defect_charge' : orm.Float(-2.), \n", - " 'defect_site' : orm.List(list=[0.,0.,0.]), # Position of the defect in crystal coordinates\n", - " 'fermi_level' : orm.Float(0.0), # Position of the Fermi level, with respect to the valence band maximum \n", - " 'chemical_potential' : orm.Float(250.709), # eV, the chemical potentical of a C atom\n", + " 'defect_charge' : orm.Float(-3.0),\n", + " 'defect_site' : orm.List(list=defect_position), # Position of the defect in crystal coordinates\n", + " 'chempot_sign': orm.Dict(dict={'Al':-1}), \n", + " 'run_chem_pot_wc' : orm.Bool(False),\n", + " 'chemical_potential' : orm.Dict(dict={'Al':-524.03435, 'P':-191.03878}),\n", + " 'fermi_level' : orm.Float(0.0), # Fermi level is set to zero by default\n", + " # Setup\n", + " 'run_pw_host' : orm.Bool(run_pw),\n", + " 'run_pw_defect_q0' : orm.Bool(run_pw),\n", + " 'run_pw_defect_q' : orm.Bool(run_pw),\n", + " 'run_v_host' : orm.Bool(run_v),\n", + " 'run_v_defect_q0' : orm.Bool(run_v),\n", + " 'run_v_defect_q' : orm.Bool(run_v),\n", + " 'run_rho_host' : orm.Bool(run_rho),\n", + " 'run_rho_defect_q0' : orm.Bool(run_rho),\n", + " 'run_rho_defect_q' : orm.Bool(run_rho), \n", + " 'run_dfpt' : orm.Bool(False),\n", " # Method\n", " 'correction_scheme' : orm.Str('gaussian'),\n", + " 'epsilon' : dielectric, # epsilon_inf = 3.2\n", + " 'cutoff' : orm.Float(400.0),\n", + " 'charge_model': {\n", + " 'model_type': orm.Str('fixed'),\n", + " 'fixed':{\n", + " 'covariance_matrix': cov_matrix }\n", + " # 'fitted': {\n", + " # 'tolerance': orm.Float(1.0e-3),\n", + " # 'strict_fit': orm.Bool(True),\n", + " # }\n", + " },\n", " # Computational (chosen code is QE)\n", " 'qe' : {\n", " 'dft': {\n", " 'supercell' : {\n", " 'code' : pw_code,\n", - " 'kpoints': kpoints, \n", - " 'pseudopotentials': pseudos, \n", - " 'parameters' : pw_parameters,\n", + " #'kpoints': kpoints, \n", + " 'pseudopotential_family': pseudo_family, \n", + " # 'parameters' : pw_host_parameters,\n", " 'scheduler_options' : pw_metadata,\n", + " 'settings' : pw_settings,\n", "\n", " },\n", " 'unitcell' : {\n", " 'code' : pw_code,\n", - " 'kpoints': kpoints_unitcell, \n", - " 'pseudopotentials': pseudos, \n", - " 'parameters' : pw_parameters,\n", + " #'kpoints': kpoints_unitcell, \n", + " 'pseudopotential_family': pseudo_family, \n", + " #'parameters' : pw_parameters,\n", " 'scheduler_options' : pw_metadata,\n", + " 'settings' : pw_settings,\n", " } \n", " },\n", - " 'dfpt' : {\n", - " 'code' : ph_code,\n", - " 'scheduler_options' : ph_metadata,\n", - " \n", - " },\n", + "# 'dfpt' : {\n", + "# 'code' : ph_code,\n", + "# 'scheduler_options' : ph_metadata,\n", + "# },\n", " 'pp' : {\n", " 'code' : pp_code,\n", - " 'scheduler_options' : pw_metadata,\n", + " 'scheduler_options' : pp_metadata,\n", " }\n", " }\n", - "}" + "}\n" ] }, { @@ -349,25 +321,55 @@ " else:\n", " print('Not yet finished')\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Optional**\n", + "If the dielectric constant of the host materials is not know and you wish to compute it as part of the workchain, you have to specify the ph code as you did with pw and pp codes.\n", + "\n", + "ph_code = orm.Code.get_from_string('ph@localhost')\n", + "\n", + "\n", + "ph_metadata = orm.Dict( dict={\n", + " 'description': 'Li3ClO test', \n", + " 'options': {\n", + " 'max_wallclock_seconds': 1800, \n", + " 'resources': {\n", + " 'num_machines': 12\n", + " },\n", + " 'queue_name' : 'debug'\n", + " }, \n", + " 'label': 'Li3ClO test'\n", + "})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 2", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "python2" + "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.10" + "pygments_lexer": "ipython3", + "version": "3.8.10" } }, "nbformat": 4, diff --git a/examples/GaussianCountercharge.ipynb b/examples/GaussianCountercharge.ipynb index cf3261e..4d6637b 100644 --- a/examples/GaussianCountercharge.ipynb +++ b/examples/GaussianCountercharge.ipynb @@ -8,18 +8,7 @@ "\n", "This notebook will explain the use of the `GaussianCountercharge` Workchain. \n", "\n", - "Ths workchain implements a correction to the supercell energy based on an equivalent electrostatic model. Normally, this lower-level workchain need not be used directly as the higher-level `FormationEnergy` workchain will abstract the detail away and automate the generation of the necessary inputs. For completeness, and to give the option to use the correction directly, the use of the workchain is demonstrated below.\n", - "\n", - "**NOTE!**\n", - "In this alpha version of `AiiDA-Defects` there are a number of features which are either not yet implemented or not well tested. These are listed below, with no guarantee of this being an exhaustive list.\n", - "Please bear these considerations in mind when testing the workchain.\n", - "\n", - "* `epsilon`, the dielctric constant of the bulk host material, must be specified 'by hand' when using the GaussianCountercharge workchain directly.\n", - "* Alignment of the electrostatic potentials is not yet automatic. Placeholder code will return 0.0 eV for these steps. The alignment should be done 'by hand'. A typical option is to take a planar average of the electrostatic potential along some axis, and then to align far from the defect. For cubic cells, a convenient option is to place point defects in the center of the box, and then take the alignment at the edge. \n", - "* In principle, only cubic cells are currently supported, although for a large-enough supercell, it shouldn't matter. This would be interesting to prove/disprove. A change to any shape of supercell is on the TODO list.\n", - "* The width of the model gaussian is fixed currently, with a TODO for a routine to fit this. If one wants to play with this for testing, I can expose it as an input.\n", - "\n", - "\n" + "Ths workchain implements a correction to the supercell energy based on an equivalent electrostatic model. Normally, this lower-level workchain need not be used directly as the higher-level `FormationEnergy` workchain will abstract the detail away and automate the generation of the necessary inputs. For completeness, and to give the option to use the correction directly, the use of the workchain is demonstrated below." ] }, { @@ -32,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -55,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -73,7 +62,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -88,12 +77,18 @@ "\n", "# Create an arbitrary array\n", "placeholder_array = orm.ArrayData()\n", - "placeholder_array.set_array('test', np.ones(3))\n", + "placeholder_array.set_array('test', np.ones([3,3,3]))\n", "\n", "# Assign it to the inputs\n", "builder.v_host = placeholder_array\n", "builder.v_defect_q0 = placeholder_array\n", "builder.v_defect_q = placeholder_array\n", + "builder.rho_host = placeholder_array\n", + "builder.rho_defect_q = placeholder_array\n", + "\n", + "builder.charge_model.model_type = orm.Str('fixed')\n", + "builder.epsilon = placeholder_array # Dielectric constant of the host material\n", + "builder.charge_model.fixed.covariance_matrix = placeholder_array \n", "\n", "# Prepare a structre. Only the host structure is required as the user sets the defect location explicitly.\n", "# Here, a dummy strucute data with no atoms is used, but any valid StructureData object can passed in. \n", @@ -117,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -170,6 +165,24 @@ "The modelled interaction (q/r) is smooth and so good results are possible at the default of 40 Ry. In principle, increasing this value improves the accuracy of the results, in exchange for increased computational expense, but equally, the overall method has low sensitivity to this parameter.\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "array_shapes=[(3,3,3),(3,3,3)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(set(array_shapes))" + ] + }, { "cell_type": "code", "execution_count": null, @@ -180,21 +193,21 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 2", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "python2" + "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.10" + "pygments_lexer": "ipython3", + "version": "3.8.10" } }, "nbformat": 4, diff --git a/examples/Structures/Li3ClO_1x1x1.cif b/examples/Structures/Li3ClO_1x1x1.cif new file mode 100644 index 0000000..07181fa --- /dev/null +++ b/examples/Structures/Li3ClO_1x1x1.cif @@ -0,0 +1,37 @@ +#====================================================================== + +# CRYSTAL DATA + +#---------------------------------------------------------------------- + +data_VESTA_phase_1 + + +_chemical_name_common 'Li3 Cl1 O1' +_cell_length_a 3.90834 +_cell_length_b 3.90834 +_cell_length_c 3.90834 +_cell_angle_alpha 90 +_cell_angle_beta 90 +_cell_angle_gamma 90 +_space_group_name_H-M_alt 'P 1' +_space_group_IT_number 1 + +loop_ +_space_group_symop_operation_xyz + 'x, y, z' + +loop_ + _atom_site_label + _atom_site_occupancy + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_adp_type + _atom_site_B_iso_or_equiv + _atom_site_type_symbol + Li0 1.0 0.500000 0.000000 0.500000 Biso 1.000000 Li + Li1 1.0 0.000000 0.500000 0.500000 Biso 1.000000 Li + Li2 1.0 0.000000 0.000000 0.000000 Biso 1.000000 Li + Cl3 1.0 0.500000 0.500000 0.000000 Biso 1.000000 Cl + O4 1.0 0.000000 0.000000 0.500000 Biso 1.000000 O diff --git a/setup.json b/setup.json index 34ceaba..f992060 100644 --- a/setup.json +++ b/setup.json @@ -1,7 +1,7 @@ { "name": "aiida-defects", - "version": "0.6.0", - "author": "Conrad Johnston, Chiara Ricca", + "version": "0.9.0", + "author": "Conrad Johnston, Sokseiha Muy, Chiara Ricca", "author_email": "conrad.s.johnston@googlemail.com", "url": "https://github.com/ConradJohnston/aiida-defects/", "license": "MIT License", @@ -14,8 +14,13 @@ "Framework :: AiiDA" ], "entry_points": { + "aiida.data": [ + "array.stability = aiida_defects.data.data:StabilityData" + ], "aiida.workflows": [ "defects.formation_energy.qe = aiida_defects.formation_energy.formation_energy_qe:FormationEnergyWorkchainQE", + "defects.formation_energy.chemical_potential = aiida_defects.formation_energy.chemical_potential.chemical_potential:ChemicalPotentialWorkchain", + "defects.formation_energy.siesta = aiida_defects.formation_energy.formation_energy_siesta:FormationEnergyWorkchainSiesta", "defects.formation_energy.corrections.gaussian_countercharge = aiida_defects.formation_energy.corrections.gaussian_countercharge.gaussian_countercharge:GaussianCounterChargeWorkchain", "defects.formation_energy.corrections.gaussian_countercharge.model_potential = aiida_defects.formation_energy.corrections.gaussian_countercharge.model_potential.model_potential:ModelPotentialWorkchain", "defects.formation_energy.corrections.point_countercharge = aiida_defects.formation_energy.corrections.point_countercharge.point_countercharge:PointCounterChargeWorkchain", @@ -27,22 +32,20 @@ "reentry_register": true, "install_requires": [ "aiida-core >= 1.0.0b1,<2.0.0", - "aiida-quantumespresso>3.0.0a1", - "pymatgen", - "six" + "aiida-quantumespresso>='3.1.0'", + "pymatgen" ], "extras_require": { "dev_precommit": [ "pre-commit", - "pylint==1.9.4; python_version<'3.0'", "pylint==2.2.2; python_version>='3.0'", "prospector==1.1.5", "pep8-naming==0.4.1", "modernize==0.7" - ], + ], "docs": [ - "Sphinx", - "docutils", + "Sphinx", + "docutils", "sphinx_rtd_theme" ], "testing": [ diff --git a/aiida_defects/tests/README.rst b/tests/README.rst similarity index 100% rename from aiida_defects/tests/README.rst rename to tests/README.rst diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..0ede96f --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..bd7915b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,385 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +# pylint: disable=redefined-outer-name,too-many-statements +"""Initialise a text database and profile for pytest.""" +import collections +import io +import os +import shutil + +import pytest + +pytest_plugins = ['aiida.manage.tests.pytest_fixtures'] # pylint: disable=invalid-name + + +@pytest.fixture(scope='session') +def filepath_tests(): + """Return the absolute filepath of the `tests` folder. + + .. warning:: if this file moves with respect to the `tests` folder, the implementation should change. + + :return: absolute filepath of `tests` folder which is the basepath for all test resources. + """ + return os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture +def filepath_fixtures(filepath_tests): + """Return the absolute filepath to the directory containing the file `fixtures`.""" + return os.path.join(filepath_tests, 'fixtures') + + +@pytest.fixture(scope='function') +def fixture_sandbox(): + """Return a `SandboxFolder`.""" + from aiida.common.folders import SandboxFolder + with SandboxFolder() as folder: + yield folder + + + +@pytest.fixture +def generate_calc_job(): + """Fixture to construct a new `CalcJob` instance and call `prepare_for_submission` for testing `CalcJob` classes. + + The fixture will return the `CalcInfo` returned by `prepare_for_submission` and the temporary folder that was passed + to it, into which the raw input files will have been written. + """ + + def _generate_calc_job(folder, entry_point_name, inputs=None): + """Fixture to generate a mock `CalcInfo` for testing calculation jobs.""" + from aiida.engine.utils import instantiate_process + from aiida.manage.manager import get_manager + from aiida.plugins import CalculationFactory + + manager = get_manager() + runner = manager.get_runner() + + process_class = CalculationFactory(entry_point_name) + process = instantiate_process(runner, process_class, **inputs) + + calc_info = process.prepare_for_submission(folder) + + return calc_info + + return _generate_calc_job + + +@pytest.fixture +def generate_calc_job_node(fixture_localhost): + """Fixture to generate a mock `CalcJobNode` for testing parsers.""" + + def flatten_inputs(inputs, prefix=''): + """Flatten inputs recursively like :meth:`aiida.engine.processes.process::Process._flatten_inputs`.""" + flat_inputs = [] + for key, value in inputs.items(): + if isinstance(value, collections.Mapping): + flat_inputs.extend(flatten_inputs(value, prefix=prefix + key + '__')) + else: + flat_inputs.append((prefix + key, value)) + return flat_inputs + + def _generate_calc_job_node( + entry_point_name='base', computer=None, test_name=None, inputs=None, attributes=None, retrieve_temporary=None + ): + """Fixture to generate a mock `CalcJobNode` for testing parsers. + + :param entry_point_name: entry point name of the calculation class + :param computer: a `Computer` instance + :param test_name: relative path of directory with test output files in the `fixtures/{entry_point_name}` folder. + :param inputs: any optional nodes to add as input links to the corrent CalcJobNode + :param attributes: any optional attributes to set on the node + :param retrieve_temporary: optional tuple of an absolute filepath of a temporary directory and a list of + filenames that should be written to this directory, which will serve as the `retrieved_temporary_folder`. + For now this only works with top-level files and does not support files nested in directories. + :return: `CalcJobNode` instance with an attached `FolderData` as the `retrieved` node. + """ + from aiida import orm + from aiida.common import LinkType + from aiida.plugins.entry_point import format_entry_point_string + + if computer is None: + computer = fixture_localhost + + filepath_folder = None + + if test_name is not None: + basepath = os.path.dirname(os.path.abspath(__file__)) + filename = os.path.join(entry_point_name[len('quantumespresso.'):], test_name) + filepath_folder = os.path.join(basepath, 'parsers', 'fixtures', filename) + filepath_input = os.path.join(filepath_folder, 'aiida.in') + + entry_point = format_entry_point_string('aiida.calculations', entry_point_name) + + node = orm.CalcJobNode(computer=computer, process_type=entry_point) + node.set_attribute('input_filename', 'aiida.in') + node.set_attribute('output_filename', 'aiida.out') + node.set_attribute('error_filename', 'aiida.err') + node.set_option('resources', {'num_machines': 1, 'num_mpiprocs_per_machine': 1}) + node.set_option('max_wallclock_seconds', 1800) + + if attributes: + node.set_attribute_many(attributes) + + if filepath_folder: + from qe_tools.utils.exceptions import ParsingError + from aiida_quantumespresso.tools.pwinputparser import PwInputFile + try: + parsed_input = PwInputFile(filepath_input) + except ParsingError: + pass + else: + inputs['structure'] = parsed_input.get_structuredata() + inputs['parameters'] = orm.Dict(dict=parsed_input.namelists) + + if inputs: + metadata = inputs.pop('metadata', {}) + options = metadata.get('options', {}) + + for name, option in options.items(): + node.set_option(name, option) + + for link_label, input_node in flatten_inputs(inputs): + input_node.store() + node.add_incoming(input_node, link_type=LinkType.INPUT_CALC, link_label=link_label) + + node.store() + + if retrieve_temporary: + dirpath, filenames = retrieve_temporary + for filename in filenames: + shutil.copy(os.path.join(filepath_folder, filename), os.path.join(dirpath, filename)) + + if filepath_folder: + retrieved = orm.FolderData() + retrieved.put_object_from_tree(filepath_folder) + + # Remove files that are supposed to be only present in the retrieved temporary folder + if retrieve_temporary: + for filename in filenames: + retrieved.delete_object(filename) + + retrieved.add_incoming(node, link_type=LinkType.CREATE, link_label='retrieved') + retrieved.store() + + remote_folder = orm.RemoteData(computer=computer, remote_path='/tmp') + remote_folder.add_incoming(node, link_type=LinkType.CREATE, link_label='remote_folder') + remote_folder.store() + + return node + + return _generate_calc_job_node + + +# @pytest.fixture(scope='session') +# def generate_upf_data(filepath_tests): +# """Return a `UpfData` instance for the given element a file for which should exist in `tests/fixtures/pseudos`.""" + +# def _generate_upf_data(element): +# """Return `UpfData` node.""" +# from aiida.orm import UpfData + +# filepath = os.path.join(filepath_tests, 'fixtures', 'pseudos', '{}.upf'.format(element)) + +# with io.open(filepath, 'r') as handle: +# upf = UpfData(file=handle.name) + +# return upf + +# return _generate_upf_data + + +# @pytest.fixture +# def generate_structure(): +# """Return a `StructureData` representing bulk silicon.""" + +# def _generate_structure(): +# """Return a `StructureData` representing bulk silicon.""" +# from aiida.orm import StructureData + +# param = 5.43 +# cell = [[param / 2., param / 2., 0], [param / 2., 0, param / 2.], [0, param / 2., param / 2.]] +# structure = StructureData(cell=cell) +# structure.append_atom(position=(0., 0., 0.), symbols='Si', name='Si') +# structure.append_atom(position=(param / 4., param / 4., param / 4.), symbols='Si', name='Si') + +# return structure + +# return _generate_structure + + +# @pytest.fixture +# def generate_kpoints_mesh(): +# """Return a `KpointsData` node.""" + +# def _generate_kpoints_mesh(npoints): +# """Return a `KpointsData` with a mesh of npoints in each direction.""" +# from aiida.orm import KpointsData + +# kpoints = KpointsData() +# kpoints.set_kpoints_mesh([npoints] * 3) + +# return kpoints + +# return _generate_kpoints_mesh + + +@pytest.fixture(scope='session') +def generate_parser(): + """Fixture to load a parser class for testing parsers.""" + + def _generate_parser(entry_point_name): + """Fixture to load a parser class for testing parsers. + + :param entry_point_name: entry point name of the parser class + :return: the `Parser` sub class + """ + from aiida.plugins import ParserFactory + return ParserFactory(entry_point_name) + + return _generate_parser + + +@pytest.fixture +def generate_remote_data(): + """Return a `RemoteData` node.""" + + def _generate_remote_data(computer, remote_path, entry_point_name=None): + """Return a `KpointsData` with a mesh of npoints in each direction.""" + from aiida.common.links import LinkType + from aiida.orm import CalcJobNode, RemoteData + from aiida.plugins.entry_point import format_entry_point_string + + entry_point = format_entry_point_string('aiida.calculations', entry_point_name) + + remote = RemoteData(remote_path=remote_path) + remote.computer = computer + + if entry_point_name is not None: + creator = CalcJobNode(computer=computer, process_type=entry_point) + creator.set_option('resources', {'num_machines': 1, 'num_mpiprocs_per_machine': 1}) + remote.add_incoming(creator, link_type=LinkType.CREATE, link_label='remote_folder') + creator.store() + + return remote + + return _generate_remote_data + + +@pytest.fixture +def generate_workchain(): + """Generate an instance of a `WorkChain`.""" + + def _generate_workchain(entry_point, inputs): + """Generate an instance of a `WorkChain` with the given entry point and inputs. + + :param entry_point: entry point name of the work chain subclass. + :param inputs: inputs to be passed to process construction. + :return: a `WorkChain` instance. + """ + from aiida.engine.utils import instantiate_process + from aiida.manage.manager import get_manager + from aiida.plugins import WorkflowFactory + + process_class = WorkflowFactory(entry_point) + runner = get_manager().get_runner() + process = instantiate_process(runner, process_class, **inputs) + + return process + + return _generate_workchain + + + + +# New +@pytest.fixture +def generate_array_data(): + """Return an `ArrayData` node.""" + + def _generate_array_data(size): + """Return a `ArrayData` with an identity matrix of dimension `size`.""" + from aiida.orm import ArrayData + from numpy import ones + + array = ArrayData() + array.set_array('test', ones(size)) + + return array + + return _generate_array_data + +## Methods below not new but retained from above + +@pytest.fixture +def generate_structure(): + """Return a `StructureData` representing bulk silicon.""" + + def _generate_structure(): + """Return a `StructureData` representing bulk silicon.""" + from aiida.orm import StructureData + + param = 5.43 + cell = [[param / 2., param / 2., 0], [param / 2., 0, param / 2.], [0, param / 2., param / 2.]] + structure = StructureData(cell=cell) + structure.append_atom(position=(0., 0., 0.), symbols='Si', name='Si') + structure.append_atom(position=(param / 4., param / 4., param / 4.), symbols='Si', name='Si') + + return structure + + return _generate_structure + +@pytest.fixture +def generate_kpoints_mesh(): + """Return a `KpointsData` node.""" + + def _generate_kpoints_mesh(npoints): + """Return a `KpointsData` with a mesh of npoints in each direction.""" + from aiida.orm import KpointsData + + kpoints = KpointsData() + kpoints.set_kpoints_mesh([npoints] * 3) + + return kpoints + + return _generate_kpoints_mesh + +@pytest.fixture(scope='session') +def generate_upf_data(filepath_tests): + """Return a `UpfData` instance for the given element a file for which should exist in `tests/fixtures/pseudos`.""" + + def _generate_upf_data(element): + """Return `UpfData` node.""" + from aiida.orm import UpfData + + filepath = os.path.join(filepath_tests, 'fixtures', 'pseudos', '{}.upf'.format(element)) + + with io.open(filepath, 'r') as handle: + upf = UpfData(file=handle.name) + + return upf + + return _generate_upf_data + + +@pytest.fixture +def fixture_localhost(aiida_localhost): + """Return a localhost `Computer`.""" + localhost = aiida_localhost + localhost.set_default_mpiprocs_per_machine(1) + return localhost + +@pytest.fixture +def fixture_code(fixture_localhost): + """Return a `Code` instance configured to run calculations of given entry point on localhost `Computer`.""" + + def _fixture_code(entry_point_name): + from aiida.orm import Code + return Code(input_plugin_name=entry_point_name, remote_computer_exec=[fixture_localhost, '/bin/true']) + + return _fixture_code \ No newline at end of file diff --git a/tests/fixtures/pseudos/Si.upf b/tests/fixtures/pseudos/Si.upf new file mode 100644 index 0000000..bff3273 --- /dev/null +++ b/tests/fixtures/pseudos/Si.upf @@ -0,0 +1,91 @@ + + + WARNING: this is a modified dummy pseudo for unit testing purposes only + Author: ADC + Generation date: 10Oct2014 + Pseudopotential type: USPP + Element: Si + Functional: PBE + + Suggested minimum cutoff for wavefunctions: 44. Ry + Suggested minimum cutoff for charge density: 175. Ry + The Pseudo was generated with a Scalar-Relativistic Calculation + Local Potential by smoothing AE potential with Bessel fncs, cutoff radius: 1.9000 + + Valence configuration: + nl pn l occ Rcut Rcut US E pseu + 3S 1 0 2.00 1.600 1.800 -0.794728 + 3P 2 1 2.00 1.600 1.800 -0.299965 + Generation configuration: + 3S 1 0 2.00 1.600 1.800 -0.794724 + 3S 1 0 0.00 1.600 1.800 6.000000 + 3P 2 1 2.00 1.600 1.800 -0.299964 + 3P 2 1 0.00 1.600 1.800 6.000000 + 3D 3 2 0.00 1.600 1.800 0.100000 + 3D 3 2 0.00 1.600 1.800 0.300000 + + Pseudization used: troullier-martins + + &input + title='Si', + zed=14., + rel=1, + config='[Ne] 3s2 3p2 3d-1', + iswitch=3, + dft='PBE' + / + &inputp + lpaw=.false., + pseudotype=3, + file_pseudopw='Si.pbe-n-rrkjus_psl.1.0.0.UPF', + author='ADC', + lloc=-1, + rcloc=1.9, + which_augfun='PSQ', + rmatch_augfun_nc=.true., + nlcc=.true., + new_core_ps=.true., + rcore=1.3, + tm=.true. + / +6 +3S 1 0 2.00 0.00 1.60 1.80 0.0 +3S 1 0 0.00 6.00 1.60 1.80 0.0 +3P 2 1 2.00 0.00 1.60 1.80 0.0 +3P 2 1 0.00 6.00 1.60 1.80 0.0 +3D 3 2 0.00 0.10 1.60 1.80 0.0 +3D 3 2 0.00 0.30 1.60 1.80 0.0 + + + + + + + + \ No newline at end of file diff --git a/aiida_defects/tests/formation_energy/corrections/komsa_pasquarello/test_poisson_solver.py b/tests/formation_energy/corrections/komsa_pasquarello/old_test_poisson_solver.py similarity index 100% rename from aiida_defects/tests/formation_energy/corrections/komsa_pasquarello/test_poisson_solver.py rename to tests/formation_energy/corrections/komsa_pasquarello/old_test_poisson_solver.py diff --git a/aiida_defects/tests/formation_energy/corrections/komsa_pasquarello/test_utils.py b/tests/formation_energy/corrections/komsa_pasquarello/old_test_utils.py similarity index 100% rename from aiida_defects/tests/formation_energy/corrections/komsa_pasquarello/test_utils.py rename to tests/formation_energy/corrections/komsa_pasquarello/old_test_utils.py diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..dfc0b4e --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +filterwarnings = + ignore::DeprecationWarning:frozendict: + ignore::DeprecationWarning:pkg_resources: + ignore::DeprecationWarning:reentry: + ignore::DeprecationWarning:sqlalchemy_utils: \ No newline at end of file diff --git a/aiida_defects/tests/test_data/halite_bulk.cif b/tests/test_data/halite_bulk.cif similarity index 100% rename from aiida_defects/tests/test_data/halite_bulk.cif rename to tests/test_data/halite_bulk.cif diff --git a/aiida_defects/tests/test_data/halite_bulk_sub_k.cif b/tests/test_data/halite_bulk_sub_k.cif similarity index 100% rename from aiida_defects/tests/test_data/halite_bulk_sub_k.cif rename to tests/test_data/halite_bulk_sub_k.cif diff --git a/aiida_defects/tests/test_data/halite_bulk_v_cl.cif b/tests/test_data/halite_bulk_v_cl.cif similarity index 100% rename from aiida_defects/tests/test_data/halite_bulk_v_cl.cif rename to tests/test_data/halite_bulk_v_cl.cif diff --git a/aiida_defects/tests/test_data/halite_bulk_v_cl_sub_k.cif b/tests/test_data/halite_bulk_v_cl_sub_k.cif similarity index 100% rename from aiida_defects/tests/test_data/halite_bulk_v_cl_sub_k.cif rename to tests/test_data/halite_bulk_v_cl_sub_k.cif diff --git a/aiida_defects/tests/test_data/halite_unitcell.cif b/tests/test_data/halite_unitcell.cif similarity index 100% rename from aiida_defects/tests/test_data/halite_unitcell.cif rename to tests/test_data/halite_unitcell.cif diff --git a/aiida_defects/tests/test_data/lton.cif b/tests/test_data/lton.cif similarity index 100% rename from aiida_defects/tests/test_data/lton.cif rename to tests/test_data/lton.cif diff --git a/aiida_defects/tests/test_data/lton_bulk.cif b/tests/test_data/lton_bulk.cif similarity index 100% rename from aiida_defects/tests/test_data/lton_bulk.cif rename to tests/test_data/lton_bulk.cif diff --git a/aiida_defects/tests/tools/test_defects.py b/tests/tools/old_test_defects.py similarity index 100% rename from aiida_defects/tests/tools/test_defects.py rename to tests/tools/old_test_defects.py diff --git a/tests/workflows/formation_energy/corrections/gaussian_countercharge/model_potential/test_model_potential.py b/tests/workflows/formation_energy/corrections/gaussian_countercharge/model_potential/test_model_potential.py new file mode 100644 index 0000000..5ccaf85 --- /dev/null +++ b/tests/workflows/formation_energy/corrections/gaussian_countercharge/model_potential/test_model_potential.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +"""Tests for the `ModelPotentialWorkchain` class.""" +import pytest +import numpy as np + +from aiida.common import AttributeDict +from aiida.orm import Float, List, ArrayData, Dict, Int, StructureData +from aiida_defects.formation_energy.corrections.gaussian_countercharge.model_potential.model_potential import ModelPotentialWorkchain + +@pytest.fixture +def generate_inputs_model_potential(generate_structure, generate_array_data): + """Generate default inputs for `ModelPotentialWorkchain`""" + + def _generate_inputs_model_potential(): + """Generate default inputs for `ModelPotentialWorkchain`""" + + mock_array = generate_array_data(3) + + inputs = { + 'peak_charge': Float(1.0), + 'defect_charge': Float(1.0), + 'scale_factor': Int(2), + 'host_structure': generate_structure(), + 'defect_site': List(list=[0.5,0.5,0.5]), + 'epsilon': mock_array, + 'gaussian_params': List(list=[1.,1.,1.,1.,1.,1.,1.,1.,1.]) + } + + return inputs + + return _generate_inputs_model_potential + + + +@pytest.fixture +def generate_workchain_model_potential(generate_workchain, generate_inputs_model_potential): + """Generate an instance of a `ModelPotentialWorkchain`.""" + + def _generate_workchain_model_potential(exit_code=None): + entry_point = 'defects.formation_energy.corrections.gaussian_countercharge.model_potential' + inputs = generate_inputs_model_potential() + process = generate_workchain(entry_point, inputs) + + if exit_code is not None: + node.set_process_state(ProcessState.FINISHED) + node.set_exit_status(exit_code.status) + + return process + + return _generate_workchain_model_potential + +def test_get_model_structure(aiida_profile, generate_workchain_model_potential): + """ + Test `ModelPotentialWorkchain.get_model_structure`. + This checks that we can create the workchain successfully, and that model structure + is created correctly. + """ + from numpy import ndarray + + process = generate_workchain_model_potential() + process.get_model_structure() + + assert isinstance(process.ctx.model_structure, StructureData) + assert isinstance(process.ctx.real_cell, ndarray) + assert isinstance(process.ctx.reciprocal_cell, ndarray) + assert isinstance(process.ctx.limits, List) \ No newline at end of file diff --git a/tests/workflows/formation_energy/corrections/gaussian_countercharge/test_gaussian_countercharge.py b/tests/workflows/formation_energy/corrections/gaussian_countercharge/test_gaussian_countercharge.py new file mode 100644 index 0000000..504b0a6 --- /dev/null +++ b/tests/workflows/formation_energy/corrections/gaussian_countercharge/test_gaussian_countercharge.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +"""Tests for the `GaussianCounterChargeWorkchain` class.""" +import pytest +from aiida.common import AttributeDict +from aiida.orm import Float, List, Dict, Int, Str +from aiida_defects.formation_energy.corrections.gaussian_countercharge.gaussian_countercharge import GaussianCounterChargeWorkchain + +@pytest.fixture +def generate_inputs_gaussian_countercharge(generate_structure, generate_array_data): + """Generate default inputs for `GaussianCounterChargeWorkchain`""" + + def _generate_inputs_gaussian_countercharge(): + """Generate default inputs for `GaussianCounterChargeWorkchain`""" + + mock_array = generate_array_data(3) + + inputs = { + 'host_structure' : generate_structure(), + 'defect_charge' : Float(-2.), + 'defect_site' : List(list=[0.5,0.5,0.5]), + 'epsilon' : mock_array, + 'v_host' : mock_array, + 'v_defect_q0' : mock_array, + 'v_defect_q' : mock_array, + 'rho_host' : mock_array, + 'rho_defect_q' : mock_array, + 'charge_model': { + 'model_type': Str('fitted'), + 'fitted': {} + } + } + + return inputs + + return _generate_inputs_gaussian_countercharge + + + +@pytest.fixture +def generate_workchain_gaussian_countercharge(generate_workchain, generate_inputs_gaussian_countercharge): + """Generate an instance of a `GaussianCounterChargeWorkchain`.""" + + def _generate_workchain_gaussian_countercharge(exit_code=None): + entry_point = 'defects.formation_energy.corrections.gaussian_countercharge' + inputs = generate_inputs_gaussian_countercharge() + process = generate_workchain(entry_point, inputs) + + if exit_code is not None: + node.set_process_state(ProcessState.FINISHED) + node.set_exit_status(exit_code.status) + + return process + + return _generate_workchain_gaussian_countercharge + +def test_setup(aiida_profile, generate_workchain_gaussian_countercharge): + """ + Test `GaussianCounterChargeWorkchain.setup`. + This checks that we can create the workchain successfully, and that it is initialised in to the correct state. + """ + process = generate_workchain_gaussian_countercharge() + process.setup() + + # assert process.ctx.restart_calc is None + assert process.ctx.model_iteration.value == 0 + assert process.ctx.model_energies == {} + assert process.ctx.model_structures == {} + assert process.ctx.model_correction_energies == {} \ No newline at end of file diff --git a/tests/workflows/formation_energy/test_formation_energy_qe.py b/tests/workflows/formation_energy/test_formation_energy_qe.py new file mode 100644 index 0000000..d8e3fac --- /dev/null +++ b/tests/workflows/formation_energy/test_formation_energy_qe.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +"""Tests for the `FormationEnergyWorkchainQE` class.""" +import pytest +from aiida.common import AttributeDict +from aiida.orm import Float, List, Dict, Int, Str, Bool +from aiida_defects.formation_energy.formation_energy_qe import FormationEnergyWorkchainQE + +@pytest.fixture +def generate_inputs_formation_energy_qe(fixture_code, generate_structure, generate_kpoints_mesh, generate_array_data, generate_upf_data): + """Generate default inputs for `FormationEnergyWorkchainQE`""" + + def _generate_inputs_formation_energy_qe(): + """Generate default inputs for `FormationEnergyWorkchainQE`""" + + mock_array = generate_array_data(3) + mock_structure = generate_structure() + mock_parameters = Dict(dict={}) + mock_kpoints = generate_kpoints_mesh(2) + mock_pseudos = {'Si': generate_upf_data('Si')} + + inputs = { + 'run_pw_host': Bool(True), + 'run_pw_defect_q0': Bool(True), + 'run_pw_defect_q': Bool(True), + 'run_v_host': Bool(True), + 'run_v_defect_q0': Bool(True), + 'run_v_defect_q': Bool(True), + 'run_rho_host': Bool(True), + 'run_rho_defect_q0': Bool(True), + 'run_rho_defect_q': Bool(True), + 'run_dfpt': Bool(True), + "host_structure": mock_structure, + "defect_structure": mock_structure, + "host_unitcell": mock_structure, + 'defect_charge': Float(1.0), + 'defect_species': Str('Si'), + 'defect_site': List(list=[0.5,0.5,0.5]), + "fermi_level": Float(1.0), + "chempot_sign": Dict(dict={}), + "formation_energy_dict": Dict(dict={}), + "compound": Str("SiO2"), + "dependent_element": Str("O"), + "correction_scheme": Str('gaussian'), + "run_dfpt": Bool(True), + 'epsilon' : mock_array, + 'run_pw_host': Bool(True), + 'run_pw_defect_q0': Bool(True), + 'run_pw_defect_q': Bool(True), + "qe": { + "dft": { + "supercell": { + "code": fixture_code('quantumespresso.pw'), + "parameters": mock_parameters, + "scheduler_options": mock_parameters, + "pseudopotential_family": Str("sssp"), + "settings": mock_parameters, + }, + "unitcell": { + "code": fixture_code('quantumespresso.pw'), + "parameters": mock_parameters, + "scheduler_options": mock_parameters, + "pseudopotential_family": Str("sssp"), + "settings": mock_parameters, + }, + }, + "pp":{ + "code": fixture_code('quantumespresso.pp'), + "scheduler_options": mock_parameters, + }, + "dfpt":{ + "code": fixture_code('quantumespresso.ph'), + "scheduler_options": mock_parameters, + } + } + } + + return inputs + + return _generate_inputs_formation_energy_qe + + + +@pytest.fixture +def generate_workchain_formation_energy_qe(generate_workchain, generate_inputs_formation_energy_qe): + """Generate an instance of a `FormationEnergyWorkchainQE` workchain.""" + + def _generate_workchain_formation_energy_qe(exit_code=None): + entry_point = 'defects.formation_energy.qe' + inputs = generate_inputs_formation_energy_qe() + process = generate_workchain(entry_point, inputs) + + if exit_code is not None: + node.set_process_state(ProcessState.FINISHED) + node.set_exit_status(exit_code.status) + + return process + + return _generate_workchain_formation_energy_qe + +def test_setup(aiida_profile, generate_workchain_formation_energy_qe): + """ + Test `FormationEnergyWorkchainQE.setup`. + This checks that we can create the workchain successfully, and that it is initialised into the correct state. + """ + process = generate_workchain_formation_energy_qe() + process.setup() diff --git a/tests/workflows/formation_energy/test_formation_energy_siesta.py b/tests/workflows/formation_energy/test_formation_energy_siesta.py new file mode 100644 index 0000000..cc2762e --- /dev/null +++ b/tests/workflows/formation_energy/test_formation_energy_siesta.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +######################################################################################## +# Copyright (c), The AiiDA-Defects authors. All rights reserved. # +# # +# AiiDA-Defects is hosted on GitHub at https://github.com/ConradJohnston/aiida-defects # +# For further information on the license, see the LICENSE.txt file # +######################################################################################## +"""Tests for the `FormationEnergyWorkchainSiesta` class.""" +import pytest +from aiida.common import AttributeDict +from aiida.orm import Float, List, Dict, Int, Str +from aiida_defects.formation_energy.formation_energy_siesta import FormationEnergyWorkchainSiesta + +@pytest.fixture +def generate_inputs_formation_energy_siesta(fixture_code, generate_structure, generate_kpoints_mesh, generate_array_data, generate_upf_data): + """Generate default inputs for `FormationEnergyWorkchainSiesta`""" + + def _generate_inputs_formation_energy_siesta(): + """Generate default inputs for `FormationEnergyWorkchainSiesta`""" + + mock_structure = generate_structure() + + inputs = { + "host_structure": mock_structure, + "defect_structure": mock_structure, + "host_unitcell": mock_structure, + 'defect_charge': Float(1.0), + 'defect_site': List(list=[0.5,0.5,0.5]), + "fermi_level": Float(1.0), + "add_or_remove": Str('remove'), + "formation_energy_dict": Dict(dict={}), + "compound": Str("SiO2"), + "dependent_element": Str("O"), + "correction_scheme": Str('gaussian'), + "run_dfpt": Bool(True), + 'run_pw_host': Bool(True), + 'run_pw_defect_q0': Bool(True), + 'run_pw_defect_q': Bool(True), + "siesta": { + } + } + + return inputs + + return _generate_inputs_formation_energy_siesta + + + +@pytest.fixture +def generate_workchain_formation_energy_siesta(generate_workchain, generate_inputs_formation_energy_siesta): + """Generate an instance of a `FormationEnergyWorkchainSiesta` workchain.""" + + def _generate_workchain_formation_energy_siesta(exit_code=None): + entry_point = 'defects.formation_energy.siesta' + inputs = generate_inputs_formation_energy_siesta() + process = generate_workchain(entry_point, inputs) + + if exit_code is not None: + node.set_process_state(ProcessState.FINISHED) + node.set_exit_status(exit_code.status) + + return process + + return _generate_workchain_formation_energy_siesta + +@pytest.mark.skip(reason="Siesta version of workchain not implemented") +def test_setup(aiida_profile, generate_workchain_formation_energy_siesta): + """ + Test `FormationEnergyWorkchainSiesta.setup`. + This checks that we can create the workchain successfully, and that it is initialised into the correct state. + """ + process = generate_workchain_formation_energy_siesta() + process.setup() \ No newline at end of file