Source code for defermi.thermodynamics

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Solve charge neutrality and compute defect equilibria
"""
import warnings
from monty.json import MSONable
from monty.json import jsanitize
import numpy as np

from .analysis import DefectConcentrations, SingleDefConc
from .defects import get_defect_from_string
import copy
import os.path as op
import json
import os


[docs] class Conductivity: """ Class that handles conductivity calculations. """ def __init__(self,mobilities): """ Parameters ---------- mobilities : dict Dictionary with mobility values for the defect species in [cm^2/(V*s)]. Keys must contain "electrons", "holes" and the defect species names. """ self.mobilities = mobilities
[docs] def get_conductivity(self,carrier_concentrations,defect_concentrations): """ Calculate conductivity from the concentrations of electrons, holes and defects and their mobilities. Parameters ---------- carrier_concentrations : list List of tuples with intrinsic carriers concentrations (holes,electrons) in cm^-3. defect_concentrations : list Defect concentrations in the same format as the output of DefectsAnalysis in cm^-3. Returns ------- sigma : float Conductivity in S/m. """ e = 1.60217662e-19 mob = self.mobilities cc = carrier_concentrations sigma_el = e * (mob['holes']*cc[0]+ mob['electrons']*cc[1]) dc = defect_concentrations sigma_ionic = 0 for d in dc: dname = d['name'] if dname in mob.keys(): sigma_ionic += mob[dname] * d['conc'] * abs(d['charge']) * e sigma = sigma_el + sigma_ionic * 1e02 # conversion from S/cm to S/m return sigma
[docs] def get_conductivities_from_thermodata(self,thermodata): """ Calculate conductivities from defect and carrier concentrations in thermodynamic data. Only the defect species which are present in the mobility dict are considered for the calculation of the conductivity. Parameters ---------- thermodata: ThermoData ThermoData object that contains the thermodynamic data. Returns ------- conductivities: list List with conductivity values in S/m. """ conductivities = [] for i in range(0,len(thermodata.carrier_concentrations)): carrier_concentrations = thermodata.carrier_concentrations[i] dc = thermodata.defect_concentrations[i] defect_concentrations = [] for d in dc: if d['name'] in self.mobilities.keys(): defect_concentrations.append(d) conductivity = self.get_conductivity( carrier_concentrations=carrier_concentrations, defect_concentrations=defect_concentrations) conductivities.append(conductivity) return conductivities
[docs] class DefectThermodynamics: """ Class that handles the calculations of defect equilibriua under different conditions. """ def __init__(self, defects_analysis, bulk_dos, fixed_concentrations=None, external_defects=[], xtol=1e-05, eform_kwargs={}, dconc_kwargs={}): """ Parameters ---------- defects_analysis : DefectsAnalysis DefectsAnalysis object. bulk_dos : dict or Dos Density of states to integrate. Can be provided as density of states D(E) or using effective masses. Format for effective masses is a dict with the following keys: - "m_eff_h" : holes effective mass in units of m_e (electron mass) - "m_eff_e" : electrons effective mass in units of m_h Format for explicit DOS (dictionary) with the following keys: - 'energies' : list or np.array with energy values - 'densities' : list or np.array with total density values - 'structure' : pymatgen Structure of the material, needed for DOS volume and charge normalization Alternatively, a pymatgen Dos object (Dos, CompleteDos, or FermiDos). fixed_concentrations: dict Dictionary with fixed concentrations. Keys are defect entry names in the standard format, values are the concentrations. (ex {'Vac_Na':1e20}) external_defects : list List of external defect concentrations (not present in defect entries). Must either be a list of dictionaries with {'charge': float, 'conc': float} or a list of SingleDefConc objects. xtol : float Tolerance for `bisect` (scipy) to solve charge neutrality. eform_kwargs : dict Kwargs to pass to `entry.formation_energy`. dconc_kwargs : dict Kwargs to pass to `entry.defect_concentration`. """ self.da = defects_analysis self.bulk_dos = bulk_dos self.fixed_concentrations = fixed_concentrations if fixed_concentrations else None self.external_defects = external_defects or [] self.xtol = xtol self.eform_kwargs = eform_kwargs self.dconc_kwargs = dconc_kwargs
[docs] def get_pO2_thermodata( self, reservoirs, temperature=None, name=None): """ Calculate defect and carrier concentrations as a function of the oxygen partial pressure. Parameters ---------- reservoirs : dict, Reservoirs or PressureReservoirs Object with partial pressure values as keys and chempots dictionary as values. temperature : float Temperature in Kelvin. If None reservoirs.temperature is used. name : str Name to assign to ThermoData. Returns ------- thermodata : ThermoData ThermoData object that contains the thermodynamic data: partial_pressures : (list) List of partial pressure values in atm. defect_concentrations : (list) List of DefectConcentrations objects (cm^-3) carrier_concentrations : (list) List of tuples with intrinsic carriers concentrations (holes,electrons) in cm^-3. fermi_levels : (list) list of Fermi level values in eV. """ res = reservoirs if temperature: T = temperature elif hasattr(res,'temperature'): T = res.temperature else: raise ValueError('Temperature needs to be provided or to be present ad attribute in PressureReservoirs object') partial_pressures = list(res.keys()) defect_concentrations = [] carrier_concentrations = [] fermi_levels=[] for r,mu in res.items(): single_thermodata = self.get_single_point_thermodata( chemical_potentials=mu, temperature=T ) defect_concentrations.append(single_thermodata['defect_concentrations']) carrier_concentrations.append(single_thermodata['carrier_concentrations']) fermi_levels.append(single_thermodata['fermi_levels']) data = {} data['partial_pressures'] = partial_pressures data['defect_concentrations'] = defect_concentrations data['carrier_concentrations'] = carrier_concentrations data['fermi_levels'] = fermi_levels data['temperature'] = temperature thermodata = ThermoData(data,name=name) return thermodata
[docs] def get_pO2_quenched_thermodata( self, reservoirs, initial_temperature, final_temperature, quenched_species=None, quench_elements=False, name=None): """ Calculate defect and carrier concentrations as a function of oxygen partial pressure with quenched defects. It is possible to select which defect species to quench and which ones are free to equilibrate. Frozen defects in the inputs of the class are still considered. Parameters ---------- reservoirs : dict, Reservoirs or PressureReservoirs Object with partial pressure values as keys and chempots dictionary as values. initial_temperature : float Value of initial temperature in K. final_temperature : float Value of final temperature in K. quenched_species : list List of defect species to quench. If None all defect species are quenched.The default is None. quench_elements : bool If True the total concentrations of elements at high temperature go in the charge neutrality at low temperature. If False the quenched concentrations are the ones of single defect species (e.g. elements are not allowed to equilibrate on different sites). The default is False. name : str Name to assign to ThermoData. Returns ------- thermodata : ThermoData ThermoData object that contains the thermodynamic data: partial_pressures : (list) List of partial pressure values in atm. defect_concentrations : (list) List of DefectConcentrations objects (cm^-3) carrier_concentrations : (list) List of tuples with intrinsic carriers concentrations (holes,electrons) in cm^-3. fermi_levels : (list) list of Fermi level values in eV. """ res = reservoirs if hasattr(res,'temperature'): if res.temperature != initial_temperature: warnings.warn('PressureReservoirs temperature is not set to the initial quenching temperature',UserWarning) partial_pressures = list(res.keys()) fermi_levels = [] defect_concentrations = [] carrier_concentrations = [] for r,mu in res.items(): single_quenched_thermodata = self.get_single_point_quenched_thermodata( chemical_potentials=mu, initial_temperature=initial_temperature, final_temperature=final_temperature, quenched_species=quenched_species, quench_elements=quench_elements) carrier_concentrations.append(single_quenched_thermodata['carrier_concentrations']) defect_concentrations.append(single_quenched_thermodata['defect_concentrations']) fermi_levels.append(single_quenched_thermodata['fermi_levels']) data = {} data['partial_pressures'] = partial_pressures data['fermi_levels'] = fermi_levels data['defect_concentrations'] = defect_concentrations data['carrier_concentrations'] = carrier_concentrations data['temperature'] = (initial_temperature,final_temperature) thermodata = ThermoData(data,name=name) return thermodata
[docs] def get_single_point_thermodata(self, chemical_potentials, temperature, fixed_concentrations=None, external_defects=None, name=None, eform_kwargs=None, dconc_kwargs=None): """ Compute carrier concentrations, defect concentrations and Fermi level for a single set of chemical potentials. Parameters ---------- chemical_potentials : dict or Chempots Chempots object containing chemical potentials. temperature : int Temperature in K. fixed_concentrations: dict Dictionary with fixed concentrations. Keys are defect entry names in the standard format, values are the concentrations. (ex {'Vac_Na':1e20}) external_defects : list List of external defect concentrations (not present in defect entries). Must either be a list of dictionaries with {'charge': float, 'conc': float} or a list of SingleDefConc objects. name : str Label for ThermoData. eform_kwargs : dict Kwargs to pass to `entry.formation_energy`. dconc_kwargs : dict Kwargs to pass to `entry.defect_concentration`. Returns ------- thermodata : ThermoData ThermoData object that contains the thermodynamic data: defect_concentrations : (DefectConcentrations) DefectConcentrations object (cm^-3). carrier_concentrations : (tuple) Tuple with intrinsic carriers concentrations in cm^-3 (holes,electrons). fermi_levels : (float) Fermi level value in eV. """ dos = self.bulk_dos fixed_df = fixed_concentrations or self.fixed_concentrations ext_df = external_defects or self.external_defects eform_kwargs = eform_kwargs if eform_kwargs is not None else self.eform_kwargs dconc_kwargs = dconc_kwargs if dconc_kwargs is not None else self.dconc_kwargs fermi_level = self.da.solve_fermi_level( chemical_potentials=chemical_potentials, bulk_dos=dos, temperature=temperature, fixed_concentrations=fixed_df, external_defects=ext_df, xtol=self.xtol, eform_kwargs=eform_kwargs, dconc_kwargs=dconc_kwargs) carrier_concentrations = self.da.carrier_concentrations( bulk_dos=dos, temperature=temperature, fermi_level=fermi_level) defect_concentrations = self.da.defect_concentrations( chemical_potentials=chemical_potentials, temperature=temperature, fermi_level=fermi_level, fixed_concentrations=fixed_df, eform_kwargs=eform_kwargs, **dconc_kwargs) if ext_df: for df in ext_df: defect_concentrations.append(df) data = {'carrier_concentrations':carrier_concentrations, 'defect_concentrations':defect_concentrations, 'fermi_levels':fermi_level} return ThermoData(data,name=name)
[docs] def get_single_point_quenched_thermodata( self, chemical_potentials, initial_temperature, final_temperature, quenched_species=None, quench_elements=False, fixed_concentrations=None, external_defects=None, name=None): """ Compute carrier concentrations, defect concentrations and Fermi level for a single set of chemical potentials. Parameters ---------- chemical_potentials : dict or Chempots Chempots object containing chemical potentials. initial_temperature : float Initial temperature in K. final_temperature : float Final temperature in K quenched_species : list List of defect species to quench. If None all defect species are quenched. quench_elements : bool If True the total concentrations of elements at high temperature go in the charge neutrality at low temperature. If False the quenched concentrations are the ones of single defect species (e.g. elements are not allowed to equilibrate on different sites). fixed_concentrations : dict Dictionary with fixed concentrations. Keys are defect entry names in the standard format, values are the concentrations. (ex {'Vac_Na':1e20}) external_defects : list List of external defect concentrations (not present in defect entries). Must either be a list of dictionaries with {'charge': float, 'conc': float} or a list of SingleDefConc objects. name : str Label for ThermoData. Returns ------- thermodata : ThermoData ThermoData object that contains the thermodynamic data: defect_concentrations : (DefectConcentrations) DefectConcentrations object (cm^-3). carrier_concentrations : (tuple) Tuple with intrinsic carriers concentrations in cm^-3 (holes,electrons). fermi_levels : (float) Fermi level value in eV. """ fixed_df = fixed_concentrations or self.fixed_concentrations ext_df = external_defects or self.external_defects single_thermodata = self.get_single_point_thermodata( chemical_potentials=chemical_potentials, temperature=initial_temperature, fixed_concentrations=fixed_df, external_defects=ext_df ) if quench_elements: conc_high_T = single_thermodata['defect_concentrations'].elemental else: conc_high_T = single_thermodata['defect_concentrations'].total if quenched_species is None: quenched_concentrations = conc_high_T.copy() else: if fixed_df: quenched_concentrations = copy.deepcopy(fixed_df) else: quenched_concentrations = {} for k in quenched_species: quenched_concentrations[k] = conc_high_T[k] single_quenched_thermodata = self.get_single_point_thermodata( chemical_potentials=chemical_potentials, temperature=final_temperature, fixed_concentrations=quenched_concentrations, external_defects=ext_df ) return single_quenched_thermodata
def _update_variable_species_concentration(self,variable_defect_specie, c, fixed_df, ext_df): if type(variable_defect_specie) == str : fixed_df.update({variable_defect_specie:c}) variable_defect_specie_str = variable_defect_specie return fixed_df, ext_df, variable_defect_specie_str elif type(variable_defect_specie) == dict: vds = variable_defect_specie try: variable_defect_specie_str = get_defect_from_string(vds['name']).specie except: variable_defect_specie_str = vds['name'] if not ext_df: single_df_conc = SingleDefConc( name=vds['name'], charge=vds['charge'], conc=c) # variable concentration ext_df_update = DefectConcentrations([single_df_conc]) else: if type(ext_df) == dict: ext_df = DefectConcentrations(ext_df) ext_df_update = ext_df.copy() if vds['name'] not in ext_df_update.names: single_df_conc = SingleDefConc( name=vds['name'], charge=vds['charge'], conc=c) ext_df_update.append(single_df_conc) single_df_conc = ext_df_update.select_concentrations(name=vds['name'],charge=vds['charge'])[0] single_df_conc.conc = c return fixed_df, ext_df_update, variable_defect_specie_str else: raise ValueError('Variable species has to be either a string (if present in DefectsAnalysis) or dict (if not present in DefectsAnalysis)')
[docs] def get_variable_species_thermodata( self, variable_defect_specie, concentration_range, chemical_potentials, temperature, external_defects=[], npoints=50, name=None): """ Calculate defect and carrier concentrations as a function of the concentration of a particular defect species (usually a dopant). Parameters ---------- variable_defect_specie : str or dict Variable species. Possible formats are: - str: name or element, if the variable defect species is in defect entries. - dict : {'name':str,'charge':int or float} if the variable species is not in defect entries. concentration_range : tuple or list Range of the concentration of the variable species in cm^-3. chemical_potentials : dict or Chempots Chempots object containing chemical potentials. temperature : float Temperature. npoints : int Number of points to divide concentration range. external_defects : list List of external defect concentrations (not present in defect entries). Must either be a list of dictionaries with {'charge': float, 'conc': float} or a list of SingleDefConc objects. name : str Label for ThermoData. Returns ------- thermodata : ThermoData ThermoData object that contains the thermodynamic data: variable_defect_specie : (str) Name of variable defect species. variable_concentrations : (list) List of concentrations of variable species. defect_concentrations : (list) List of DefectConcentrations objects in the same format as the output of DefectsAnalysis. carrier_concentrations : (list) List of tuples with intrinsic carriers concentrations (holes,electrons) in cm^-3. fermi_levels : (list) List of Fermi level values in eV. """ carrier_concentrations = [] defect_concentrations = [] fermi_levels = [] concentrations = np.logspace(start=np.log10(concentration_range[0]),stop=np.log10(concentration_range[1]),num=npoints) fixed_df = self.fixed_concentrations.copy() if self.fixed_concentrations else {} ext_df = external_defects or self.external_defects if ext_df and type(ext_df) != DefectConcentrations: ext_df = DefectConcentrations(ext_df) for c in concentrations: fixed_df, ext_df, variable_defect_specie_str = self._update_variable_species_concentration( variable_defect_specie,c,fixed_df,ext_df) single_thermodata = self.get_single_point_thermodata( chemical_potentials=chemical_potentials, temperature=temperature,fixed_concentrations=fixed_df, external_defects=ext_df ) defect_concentrations.append(single_thermodata['defect_concentrations']) carrier_concentrations.append(single_thermodata['carrier_concentrations']) fermi_levels.append(single_thermodata['fermi_levels']) data = {} data['variable_defect_specie'] = variable_defect_specie_str data['variable_concentrations'] = concentrations data['defect_concentrations'] = defect_concentrations data['carrier_concentrations'] = carrier_concentrations data['fermi_levels'] = fermi_levels data['temperature'] = temperature thermodata = ThermoData(data,name=name) return thermodata
[docs] def get_variable_species_quenched_thermodata( self, variable_defect_specie, concentration_range, chemical_potentials, initial_temperature, final_temperature, quenched_species=None, quench_elements=False, external_defects=[], npoints=50, name=None): """ Calculate defect and carrier concentrations as a function of the concentration of a particular defect species (usually a dopant) with quenched defects. It is possible to select which defect species to quench and which ones are free to equilibrate. Frozen defects in the inputs of the class are still considered. Parameters ---------- variable_defect_specie : str or dict Variable species. Possible formats are: - str: name or element, if the variable defect species is in defect entries. - dict : {'name':str,'charge':int or float} if the variable species is not in defect entries. concentration_range : tuple or list Range of the concentration of the variable species in cm^-3. chemical_potentials : dict or Chempots Chempots object containing chemical potentials. initial_temperature : float Value of initial temperature in K. final_temperature : float Value of final temperature in K. quenched_species : list List of defect species to quench. If None all defect species are quenched. quench_elements : bool If True the total concentrations of elements at high temperature go in the charge neutrality at low temperature. If False the quenched concentrations are the ones of single defect species (e.g. elements are not allowed to equilibrate on different sites). external_defects : list List of external defect concentrations (not present in defect entries). Must either be a list of dictionaries with {'charge': float, 'conc': float} or a list of SingleDefConc objects. npoints : int Number of points to divide concentration range. name : str Name to assign to ThermoData. Returns ------- thermodata : ThermoData ThermoData object that contains the thermodynamic data: variable_defect_specie : (str) Name of variable defect species. variable_concentrations : (list) List of concentrations of variable species. defect_concentrations : (list) List of DefectConcentrations objects in the same format as the output of DefectsAnalysis. carrier_concentrations : (list) List of tuples with intrinsic carriers concentrations (holes,electrons) in cm^-3. fermi_levels : (list) List of Fermi level values in eV. """ carrier_concentrations = [] defect_concentrations = [] fermi_levels = [] concentrations = np.logspace(start=np.log10(concentration_range[0]),stop=np.log10(concentration_range[1]),num=npoints) fixed_df = self.fixed_concentrations.copy() if self.fixed_concentrations else {} ext_df = external_defects or self.external_defects for c in concentrations: fixed_df, ext_df, variable_defect_specie_str = self._update_variable_species_concentration( variable_defect_specie,c,fixed_df,ext_df) single_quenched_thermodata = self.get_single_point_quenched_thermodata( chemical_potentials=chemical_potentials, initial_temperature=initial_temperature, final_temperature=final_temperature, quenched_species=quenched_species, quench_elements=quench_elements, fixed_concentrations=fixed_df, external_defects=ext_df ) defect_concentrations.append(single_quenched_thermodata['defect_concentrations']) carrier_concentrations.append(single_quenched_thermodata['carrier_concentrations']) fermi_levels.append(single_quenched_thermodata['fermi_levels']) data = {} data['variable_defect_specie'] = variable_defect_specie_str data['variable_concentrations'] = concentrations data['fermi_levels'] = fermi_levels data['defect_concentrations'] = defect_concentrations data['carrier_concentrations'] = carrier_concentrations data['temperature'] = (initial_temperature,final_temperature) thermodata = ThermoData(data,name=name) return thermodata
[docs] class ThermoData(MSONable): "Class to handle defect thermodynamics data" def __init__(self,thermodata,name=None): """ Class that handles dict of defect thermodynamics data. Parameters ---------- thermodata : dict Dict that contains the thermodynamic data, typically output from the methods in the `DefectThermodynamics` class. Keys of the dict are set as attributes of ThermoData. Possible items are: partial_pressures : (list) partial pressure values. variable_defect_specie : (str) Name of variable defect species. variable_concentrations : (list) Concentrations of variable species. defect_concentrations : (list) DefectConcentrations objects (cm^-3). carrier_concentrations : (list) Tuples with intrinsic carriers concentrations (holes,electrons) in cm^-3. conductivities : (list) Conductivity values (in S/m). fermi_levels : (list) Fermi level values in eV. temperatures : (list) Temperature values in K. name : str Name to assign to ThermoData. """ self.data = thermodata self.name = name if name else None for k,v in thermodata.items(): setattr(self, k, v) def __getitem__(self,key): return self.data[key] def __iter__(self): return self.data.__iter__()
[docs] def keys(self): return self.data.keys()
[docs] def values(self): return self.data.values()
[docs] def items(self): return self.data.items()
[docs] def as_dict(self): """ Returns: Json-serializable dict representation of ThermoData """ d = {"@module": self.__class__.__module__, "@class": self.__class__.__name__, "thermodata":self.data.copy(), "name": self.name } if 'defect_concentrations' in self.data.keys(): d['thermodata']['defect_concentrations'] = [dc.as_dict() for dc in self.defect_concentrations] d = jsanitize(d) #convert numpy.float64 to float return d
[docs] def to_json(self,path=None): """ Save ThermoData object as json string or file Parameters ---------- path : str Path to the destination file. If None the name of ThermoData is used as filename. Returns ------- d : str If path is not set a string is returned. """ if not path: path = op.join(os.getcwd(),f'thermodata_{self.name}.json') d = self.as_dict() with open(path,'w') as file: json.dump(d,file) return
[docs] @classmethod def from_dict(cls,d): """ Reconstitute a ThermoData object from a dict representation created using as_dict(). Parameters ---------- d : dict Dictionary representation of ThermoData. Returns ------- ThermoData object. """ if 'thermodata' in d.keys(): data = d['thermodata'].copy() if 'defect_concentrations' in data.keys(): data['defect_concentrations'] = [DefectConcentrations.from_dict(dc) for dc in data['defect_concentrations']] name = d['name'] else: data = d # recover old name = None return cls(data,name)
[docs] @staticmethod def from_json(path_or_string): """ Build ThermoData object from json file or string. Parameters ---------- path_or_string : str If an existing path to a file is given the object is constructed reading the json file. Otherwise it will be read as a string. Returns ------- ThermoData object. """ if op.isfile(path_or_string): with open(path_or_string) as file: d = json.load(file) else: d = json.loads(path_or_string) return ThermoData.from_dict(d)
[docs] def get_specific_pressures(self,p_values): """ Get ThermoData object only for specific pressure values. The closest pressure value present in list is chosen for each provided value. Parameters ---------- p_values : list List of partial pressure values. Returns ------- ThermoData object """ seldata = {} for p in p_values: pressures = self.partial_pressures p_sel = min(pressures, key=lambda x:abs(x-p)) index = pressures.index(p_sel) for k,v in self.data.items(): for e in v: if v.index(e) == index: if k not in seldata.keys(): seldata[k] = [] seldata[k].append(e) name = self.name + 'p_' + '-'.join([str(p) for p in p_values]) if self.name else None return ThermoData(seldata,name=name)
[docs] def set_data(self,key,value): # chosen over __setitem__ to not accidentally overwrite data """ Set data dictionary. """ self.data[key] = value setattr(self, key, value) return