#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Core defect objects
"""
import importlib
import warnings
from abc import ABCMeta, abstractmethod, abstractproperty
from monty.json import MSONable
import importlib
from pymatgen.core.composition import Composition
from pymatgen.core.sites import PeriodicSite
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
from .tools.structure import is_site_in_structure, is_site_in_structure_coords
[docs]
class Defect(MSONable,metaclass=ABCMeta): #MSONable contains as_dict and from_dict methods
"""
Abstract class for a single point defect
"""
def __init__(self,
specie=None,
defect_site=None,
bulk_structure=None,
charge=None,
multiplicity=None,
bulk_volume=None,
label=None):
"""
Base class for defect objets.
Parameters
----------
specie : str
Defect element symbol.
defect_site : Site
Pymatgen Site object of the defect.
bulk_structure : Structure
Pymatgen Structure without defects.
charge : int or float
Charge of the defect.
multiplicity : int
Multiplicity of defect within the simulation cell.
bulk_volume : float
Volume of bulk cell in A°^3.
label : str
Defect label.
"""
if specie:
self._specie = specie
elif defect_site:
self._specie = defect_site.specie.symbol
else:
raise ValueError('Either defect species symbol or defect Site have to be provided')
self._defect_site = defect_site
self._bulk_structure = bulk_structure
self._charge = charge
self._multiplicity = multiplicity
self._bulk_volume = bulk_volume
self._label = label
def __repr__(self):
string = f'Defect: type={self.type}, species={self.specie}'
if self.charge is not None:
string += f', charge={self.charge}'
if self.label:
string += f', label={self.label}'
if self._defect_site:
string += ", site= {}".format(self.site.frac_coords)
return string
def __print__(self):
return self.__repr__()
def __iter__(self):
"""
Dummy iterable for integration with defect complexes
"""
return [self].__iter__()
[docs]
@staticmethod
def from_string(string, **kwargs):
if '(' in string:
name,label = string.split('(')
label = label.strip(')')
else:
name = string
label=None
if '_' not in name:
raise ValueError('Defect string must contain "_"')
nsplit = name.split('_')
ntype = nsplit[0]
el = nsplit[1]
bulk_specie = None
if ntype=='Vac':
dtype = 'Vacancy'
dspecie = el
elif ntype=='Int':
dtype = 'Interstitial'
dspecie = el
elif ntype=='Sub':
dtype = 'Substitution'
dspecie = el
el_bulk = nsplit[3]
bulk_specie = el_bulk
elif ntype=='Pol':
dtype = 'Polaron'
dspecie = el
else:
raise ValueError('Defect string does not start with one of "Vac","Int","Sub","Pol"')
module = importlib.import_module(Defect.__module__)
defect_class = getattr(module,dtype)
kwargs.update({
'specie':dspecie,
'label': label
})
if dtype=='Substitution':
kwargs['bulk_specie'] = bulk_specie
return defect_class(**kwargs)
@property
def bulk_structure(self):
"""
Structure without defects.
"""
return self._bulk_structure
@property
def bulk_volume(self):
"""
Volume of bulk cell in A°^3.
"""
if self._bulk_volume:
return self._bulk_volume
elif self.bulk_structure:
return self.bulk_structure.volume
@property
def charge(self):
"""
Charge of the defect.
"""
return self._charge
@abstractproperty
def defect_composition(self):
"""
Defect composition as a Composition object
"""
return
@abstractproperty
def defect_site_index(self):
"""
Index of the defect site in the structure
"""
return
@property
def defects(self): # dummy property to integrate Defect and DefectComplex
return [self]
@property
def specie(self):
"""
Defect species.
"""
return self._specie
@property
def defect_structure(self):
"""
Structure of the defect.
"""
return self.generate_defect_structure()
@property
def type(self):
"""
Defect type.
"""
return self.__class__.__name__
@property
def delta_atoms(self):
"""
Dictionary with defect element symbol as keys and difference in particle number
between defect and bulk structure as values
"""
return
[docs]
@abstractmethod
def get_multiplicity(self):
return
@property
def label(self):
"""
Defect label.
"""
return self._label
@property
def multiplicity(self):
"""
Multiplicity of a defect site within the structure
"""
return self._multiplicity
@abstractproperty
def name(self):
"""
Name of the defect.
"""
return
@property
def site(self):
"""
Defect position as a Site object
"""
if self._defect_site:
return self._defect_site
else:
warnings.warn('Site is not stored in Defect object')
@property
def site_concentration_in_cm3(self):
"""
Site concentration (multiplicity/volume) expressed in cm^-3.
"""
return self.multiplicity * 1e24 / self.bulk_volume
@property
def symbol(self):
"""
Latex formatted name of the defect.
"""
return
@property
def symbol_with_charge(self):
"""
Name in latex format with charge written as a number.
"""
return format_legend_with_charge_number(self.symbol,self.charge)
@property
def symbol_with_charge_kv(self):
"""
Name in latex format with charge written with Kröger and Vink notation.
"""
return format_legend_with_charge_kv(self.symbol,self.charge)
[docs]
def set_bulk_volume(self, new_volume):
"""
Sets the volume of bulk cell
"""
self._bulk_volume = new_volume
return
[docs]
def set_charge(self, new_charge=0.0):
"""
Sets the charge of the defect.
"""
self._charge = new_charge
return
[docs]
def set_label(self,new_label):
"""
Sets the label of the defect
"""
self._label = new_label
return
[docs]
def set_multiplicity(self, new_multiplicity=None):
"""
Sets the Multiplicity of the defect.
If `new_multiplicity` is not provided it is determined
with `self.get_multiplicity`
"""
if new_multiplicity is None:
new_multiplicity = self.get_multiplicity()
self._multiplicity = new_multiplicity
return
[docs]
class Vacancy(Defect):
"""
Subclass of Defect for single vacancies.
"""
@property
def defect_composition(self):
"""
Composition of the defect.
"""
temp_comp = self.bulk_structure.composition.as_dict()
temp_comp[str(self.site.specie)] -= 1
return Composition(temp_comp)
@property
def defect_site_index(self):
"""
Index of the defect site in the bulk structure
"""
_,index = is_site_in_structure_coords(self.site, self.bulk_structure)
return index
@property
def delta_atoms(self):
"""
Dictionary with element symbol as keys and difference in particle number
between defect and bulk structure as values
"""
return {self.specie:-1}
[docs]
def generate_defect_structure(self,bulk_structure=None,defect_site_index=None):
"""
Generate a structure containing the defect starting from a bulk structure.
Parameters
----------
bulk_structure : Structure
Bulk Structure. If not provided `self.bulk_structure` is used.
defect_site_index : Structure
Index of the defect site in the bulk structure. If not provided
`self.defect_site_index` is used.
Returns
-------
structure : Structure
Structure containing the defect.
"""
bulk_structure = bulk_structure if bulk_structure else self.bulk_structure
defect_site_index = defect_site_index if defect_site_index else self.defect_site_index
defect_structure = bulk_structure.copy()
defect_structure.remove_sites([defect_site_index])
return defect_structure
[docs]
def get_multiplicity(self,**kwargs):
"""
Get multiplicity of the defect in the structure.
Parameters
----------
**kwargs : dict
Kwargs to pass to `SpacegroupAnalyzer` ("symprec", "angle_tolerance")
"""
sga = SpacegroupAnalyzer(self.bulk_structure,**kwargs)
symmetrized_structure = sga.get_symmetrized_structure()
equivalent_sites = symmetrized_structure.find_equivalent_sites(self.site)
return len(equivalent_sites)
@property
def name(self):
"""
Name of the defect.
"""
name = f"Vac_{self.specie}"
if self.label:
name += f'({self.label})'
return name
@property
def symbol(self):
symbol = "$V_{%s}$" %self.specie
if self.label:
symbol += '(%s)' %self.label
return symbol
[docs]
class Substitution(Defect):
"""
Subclass of Defect for substitutional defects.
"""
def __init__(self,
specie=None,
bulk_specie=None,
defect_site=None,
bulk_structure=None,
charge=None,
multiplicity=None,
bulk_volume=None,
label=None,
site_in_bulk=None):
"""
Parameters
----------
site_in_bulk : PeriodicSite
Original Site in bulk structure were substitution took place.
"""
super().__init__(specie=specie,
defect_site=defect_site,
bulk_structure=bulk_structure,
charge=charge,
multiplicity=multiplicity,
bulk_volume=bulk_volume,
label=label)
if bulk_specie:
self._bulk_specie = bulk_specie
elif site_in_bulk:
self._bulk_specie = site_in_bulk.specie.symbol
else:
raise ValueError('Either bulk species or substitution site in bulk structure have to be provided')
self._site_in_bulk = site_in_bulk
@property
def bulk_specie(self):
return self._bulk_specie
@property
def defect_composition(self):
"""
Composition of the defect.
"""
defect_index = self.defect_site_index
comp = self.bulk_structure.composition.as_dict()
if str(self.specie) not in comp.keys():
comp[str(self.specie)] = 0
comp[str(self.specie)] += 1
comp[str(self.bulk_structure[defect_index].specie)] -= 1
return Composition(comp)
@property
def defect_site_index(self):
"""
Index of the defect site in the bulk structure.
"""
return self.bulk_structure.index(self.site_in_bulk)
@property
def delta_atoms(self):
"""
Dictionary with element symbol as keys and difference in particle number
between defect and bulk structure as values
"""
return {self.specie:1, self.bulk_specie:-1}
[docs]
def generate_defect_structure(self,bulk_structure=None,defect_site_index=None):
"""
Generate a structure containing the defect starting from a bulk structure.
Parameters
----------
bulk_structure : Structure
Bulk Structure. If not provided `self.bulk_structure` is used.
defect_site_index : Structure
Index of the defect site in the bulk structure. If not provided
`self.defect_site_index` is used.
Returns
-------
structure : Structure
Structure containing the defect.
"""
bulk_structure = bulk_structure if bulk_structure else self.bulk_structure
defect_site_index = defect_site_index if defect_site_index else self.defect_site_index
defect_structure = bulk_structure.copy()
defect_structure.replace(defect_site_index,self.specie)
return defect_structure
[docs]
def get_multiplicity(self,**kwargs):
"""
Get multiplicity of the defect in the structure
Parameters
----------
**kwargs : dict
Kwargs to pass to `SpacegroupAnalyzer` ("symprec", "angle_tolerance")
"""
sga = SpacegroupAnalyzer(self.bulk_structure,**kwargs)
symmetrized_structure = sga.get_symmetrized_structure()
equivalent_sites = symmetrized_structure.find_equivalent_sites(self.site_in_bulk)
return len(equivalent_sites)
[docs]
def get_site_in_bulk(self):
if self.bulk_structure and self.site:
try:
site = min(
self.bulk_structure.get_sites_in_sphere(self.site.coords, 0.5, include_index=True),
key=lambda x: x[1])
# there's a bug in pymatgen PeriodicNeighbour.from_dict and the specie attribute, get PeriodicSite instead
site = PeriodicSite(site.species, site.frac_coords, site.lattice)
return site
except:
return ValueError("""No equivalent site has been found in bulk, defect and bulk structures are too different.\
Try using the unrelaxed defect structure or provide bulk site manually""")
else:
return None
@property
def name(self):
"""
Name for this defect.
"""
name = f'Sub_{self.specie}_on_{self.bulk_specie}'
if self.label:
name += f'({self.label})'
return name
@property
def symbol(self):
symbol = "$%s_{%s}$" %(self.specie, self.bulk_specie)
if self.label:
symbol += '(%s)' %self.label
return symbol
@property
def site_in_bulk(self):
if self._site_in_bulk:
return self._site_in_bulk
else:
return self.get_site_in_bulk()
[docs]
class Interstitial(Defect):
"""
Subclass of Defect for interstitial defects.
"""
@property
def defect_composition(self):
"""
Composition of the defect.
"""
temp_comp = self.bulk_structure.composition.as_dict()
temp_comp[str(self.site.specie)] += 1
return Composition(temp_comp)
@property
def defect_site_index(self):
"""
Index of the defect site in the defect structure
"""
is_site,index = is_site_in_structure(self.site, self.defect_structure)
return index # more flexibility with is_site_in_structure
@property
def delta_atoms(self):
"""
Dictionary with element symbol as keys and difference in particle number
between defect and bulk structure as values
"""
return {self.specie:1}
[docs]
def generate_defect_structure(self,bulk_structure=None):
"""
Generate a structure containing the defect starting from a bulk structure.
Parameters
----------
bulk_structure : Structure
Bulk Structure. If not provided `self.bulk_structure` is used.
Returns
-------
structure : Structure
Structure containing the defect.
"""
bulk_structure = bulk_structure if bulk_structure else self.bulk_structure
defect_structure = bulk_structure.copy()
defect_structure.append(self.site.species,self.site.frac_coords)
return defect_structure
[docs]
def get_multiplicity(self):
raise NotImplementedError('Multiplicity calculation not implemented for Interstitial')
@property
def name(self):
"""
Name of the defect.
"""
name = f"Int_{self.specie}"
if self.label:
name += f'({self.label})'
return name
@property
def symbol(self):
symbol = "$%s_i$" %(self.specie)
if self.label:
symbol += '(%s)' %self.label
return symbol
[docs]
class Polaron(Defect):
"""
Subclass of Defect for polarons.
"""
def __init__(self,
specie=None,
defect_site=None,
bulk_structure=None,
charge=None,
multiplicity=None,
label=None,
bulk_volume=None,
defect_structure=None):
"""
defect_structure: Structure
Structure containing the polaron. If not provided the site index is searched
in the bulk structure, and `defect_structure` is set equal to the bulk structure.
"""
super().__init__(specie=specie,
defect_site=defect_site,
bulk_structure=bulk_structure,
charge=charge,
multiplicity=multiplicity,
bulk_volume=bulk_volume,
label=label)
self._defect_structure = defect_structure
@property
def defect_composition(self):
"""
Composition of the defect.
"""
return self.bulk_structure.composition
@property
def defect_site_index(self):
"""
Index of the defect site in the structure.
"""
return self.defect_structure.index(self.site)
@property
def delta_atoms(self):
"""
Dictionary with delement as keys and difference in particle number
between defect and bulk structure as values.
"""
return {}
[docs]
def generate_defect_structure(self,bulk_structure=None):
"""
Structure containing the polaron. If not provided the site index is searched
in the bulk structure, and `defect_structure` is set equal to the bulk structure.
"""
if self._defect_structure:
return self._defect_structure
else:
bulk_structure = bulk_structure if bulk_structure else self.bulk_structure
return bulk_structure
[docs]
def get_multiplicity(self,**kwargs):
"""
Get multiplicity of the defect in the structure.
Parameters
----------
**kwargs : dict
Kwargs to pass to `SpacegroupAnalyzer` ("symprec", "angle_tolerance")
"""
sga = SpacegroupAnalyzer(self.bulk_structure,**kwargs)
symmetrized_structure = sga.get_symmetrized_structure()
equivalent_sites = symmetrized_structure.find_equivalent_sites(self.site)
return len(equivalent_sites)
@property
def name(self):
"""
Name of the defect.
"""
name = f"Pol_{self.specie}"
if self.label:
name += f'({self.label})'
return name
@property
def symbol(self):
symbol = "$%s_{%s}$" %(self.specie,self.specie)
if self.label:
symbol += '(%s)' %self.label
return symbol
[docs]
class DefectComplex(MSONable,metaclass=ABCMeta):
def __init__(self,
defects,
bulk_structure=None,
charge=None,
multiplicity=None,
bulk_volume=None,
label=None):
"""
Class to describe defect complexes
Parameters
----------
defects : list
List of Defect objects.
bulk_structure : Structure
Pymatgen Structure of the bulk material.
charge : int or float
Charge of the defect.
multiplicity : int
Multiplicity of the defect.
"""
self._defects = defects
self._bulk_structure = bulk_structure
self._charge = charge
self._multiplicity = multiplicity
self._bulk_volume = bulk_volume
self._label = label
def __repr__(self):
string = 'DefectComplex: ['
for df in self.defects:
string += f' {df.__repr__()},'
string += ' ] '
return string
def __str__(self):
return self.__repr__()
def __iter__(self):
return self.defects.__iter__()
[docs]
@staticmethod
def from_string(string, **kwargs):
"""
Get `DefectComplex` object from string.
"""
names = string.split(';')
defects = [Defect.from_string(n) for n in names]
return DefectComplex(defects=defects, **kwargs)
@property
def bulk_structure(self):
"""
Structure without defects.
"""
return self._bulk_structure
@property
def bulk_volume(self):
"""
Volume of bulk cell in A°^3.
"""
if self._bulk_volume:
return self._bulk_volume
elif self.bulk_structure:
return self.bulk_structure.volume
@property
def charge(self):
"""
Charge of the defect.
"""
return self._charge
@property
def defects(self):
"""
List of single defects consituting the complex.
"""
return self._defects
@property
def defect_composition(self):
"""
Composition of defect structure.
"""
return self.defect_structure.composition
@property
def defect_names(self):
"""
List of names of the single defects.
"""
return [d.name for d in self.defects]
@property
def defect_structure(self):
"""
Structure containing the defect.
"""
return self.generate_defect_structure()
@property
def type(self):
"""
Defect type.
"""
return self.__class__.__name__
@property
def delta_atoms(self):
"""
Dictionary with Element as keys and particle difference between defect structure
and bulk structure as values.
"""
da_global = None
for d in self.defects:
da_single = d.delta_atoms
if da_global is None:
da_global = da_single.copy()
else:
for e in da_single:
prec = da_global[e] if e in da_global.keys() else 0
da_global[e] = prec + da_single[e]
return da_global
[docs]
def generate_defect_structure(self,bulk_structure=None):
"""
Generate a structure containing the defect starting from a bulk structure.
If not provided `self.bulk_structure` is used.
"""
bulk_structure = bulk_structure if bulk_structure else self.bulk_structure
structure = bulk_structure.copy()
for df in self.defects:
df_structure = df.generate_defect_structure(structure)
structure = df_structure.copy()
return structure
[docs]
def get_multiplicity(self):
raise NotImplementedError('Not implemented for DefectComplex')
@property
def label(self):
"""
Defect label.
"""
return self._label
@property
def multiplicity(self):
"""
Multiplicity of a defect site within the structure
"""
return self._multiplicity
@property
def name(self):
"""
Name of the defect. Behaves like a string with additional attributes.
"""
name = ';'.join([df.name for df in self.defects])
if self.label:
name += f'({self.label})'
return name
@property
def site_concentration_in_cm3(self):
"""
Site concentration (multiplicity/volume) expressed in cm^-3.
"""
return self.multiplicity * 1e24 / self.bulk_volume
@property
def sites(self):
"""
Site objects of single defects.
"""
return [d.site for d in self.defects]
@property
def symbol(self):
"""
Latex formatted name of the defect.
"""
symbol = '-'.join([df.symbol for df in self.defects]) #'-'.join([df.symbol.split('(')[0] for df in self.defects]) without single df labels
if self.label:
symbol = symbol + '(%s)'%self.label
return symbol
@property
def symbol_with_charge(self):
"""
Name in latex format with charge written as a number.
"""
return format_legend_with_charge_number(self.symbol,self.charge)
@property
def symbol_with_charge_kv(self):
"""
Name in latex format with charge written with kroger and vink notation.
"""
return format_legend_with_charge_kv(self.symbol,self.charge)
[docs]
def set_charge(self, new_charge):
"""
Sets the charge of the defect.
"""
self._charge = new_charge
return
[docs]
def set_label(self,new_label):
"""
Sets the label of the defect
"""
self._label = new_label
return
[docs]
def set_multiplicity(self, new_multiplicity):
"""
Sets the charge of the defect.
"""
self._multiplicity = new_multiplicity
return
[docs]
def get_defect_from_string(string, **kwargs):
"""
Get defect object from a string (`Defect` or `DefectComplex`).
"""
if ';' in string:
return DefectComplex.from_string(string, **kwargs)
else:
return Defect.from_string(string, **kwargs)
[docs]
def get_delta_atoms(structure_defect,structure_bulk):
"""
Build delta_atoms dictionary starting from Pymatgen Structure objects.
Parameters
----------
structure_defect : Structure
Defect structure.
structure_bulk : Structure
Bulk structure.
Returns
-------
delta_atoms : dict
Dictionary with Element as keys and delta n as values.
"""
comp_defect = structure_defect.composition
comp_bulk = structure_bulk.composition
return get_delta_atoms_from_comp(comp_defect, comp_bulk)
[docs]
def get_delta_atoms_from_comp(comp_defect,comp_bulk):
"""
Build delta_atoms dictionary starting from Composition objects.
Parameters
----------
comp_defect : Composition
Defect structure.
comp_bulk : Composition
Bulk structure.
Returns
-------
delta_atoms : dict
Dictionary with Element as keys and delta n as values.
"""
delta_atoms = {}
for el,n in comp_defect.items():
nsites_defect = n
nsites_bulk = comp_bulk[el] if el in comp_bulk.keys() else 0
delta_n = nsites_defect - nsites_bulk
if delta_n != 0:
delta_atoms[el] = delta_n
return delta_atoms