import logging
import math
from collections import namedtuple
from datetime import datetime
from functools import wraps, reduce
from multiprocessing import Pool as ProcessPool
from operator import add
from pathlib import Path
from typing import List, Union
from xml.dom import minidom
from xml.dom.minidom import DocumentType, parseString
import numpy as np
from lxml import etree
from pandas import DataFrame
from gvasp.common.base import Atoms, Lattice
from gvasp.common.constant import COLUMNS_32, COLUMNS_8, ORBITALS, RED, RESET, HIGH_SYM
from gvasp.common.error import StructureNotEqualError, GridNotEqualError, AnimationError, FrequencyError, \
AttributeNotRegisteredError, ParameterError, PotDirNotExistError
from gvasp.common.parameter import Parameter
from gvasp.common.setting import RootDir
from gvasp.common.structure import Structure
from gvasp.common.utils import remove_mapping, is_subset_recommend_pot
from gvasp.lib import dos_cython, file_bind
POTENTIAL = ['PAW_LDA', 'PAW_PBE', 'PAW_PW91', 'USPP_LDA', 'USPP_PW91']
logger = logging.getLogger(__name__)
[docs]class StructInfoFile(MetaFile):
def __new__(cls, *args, **kwargs):
if cls is StructInfoFile:
raise TypeError(f"<{cls.__name__} class> may not be instantiated")
return super().__new__(cls)
@property
def structure(self):
return Structure.from_POSCAR(self.name)
[docs]class CellFile(StructInfoFile):
@property
def structure(self): # overwrite <structure method>
return Structure.from_cell(self.name)
[docs] def to_POSCAR(self):
self.structure.write_POSCAR(name="POSCAR")
[docs]class ARCFile(MetaFile):
[docs] @staticmethod
def write(name: str, structure: List[Structure], lattice: Lattice):
a, b, c = lattice.length
alpha, beta, gamma = lattice.angle
with open(name, "w") as f:
f.write("!BIOSYM archive 3\n")
f.write("PBC=ON\n")
for frame in range(len(structure)):
atoms = structure[frame].atoms.set_coord(lattice)
f.write("Auto Generated CAR File\n")
f.write(f'!DATE {datetime.now().strftime("%a %b %d %H:%M:%S %Y")}\n')
f.write(f"PBC {a:.5f} {b:.5f} {c:.5f} {alpha:.5f} {beta:.5f} {gamma:.5f} (P1)\n")
for atom in atoms:
formula, order = atom.formula, atom.order
element = formula + str(order + 1)
x, y, z = atom.cart_coord
f.write(f"{element:5s} {x:14.10f} {y:14.10f} {z:14.10f} XXXX 1 xx {formula:2s} 0.0000\n")
f.write("end\n")
f.write("end\n")
[docs]class SubmitFile(MetaFile):
@property
def run_line(self):
for line in self.strings:
if "mpirun" in line:
return line
return
@property
def finish_line(self):
for line in self.strings:
if "finish" in line:
return line
return
[docs]class INCAR(MetaFile, Parameter):
def __init__(self, name):
super(INCAR, self).__init__(name=name)
self._init_attr()
def __getattr__(self, item):
if item in self.__dict__.keys():
return self.__dict__[item]
elif item in self.__class__.__dict__.keys():
return self.__class__.__dict__[item]
def _init_attr(self):
"""
Initialize the attributes, if attribute not found, raise AttributeNotRegisteredError
"""
for line in self.strings:
if not line.strip(): # catch blanklines
continue
if line.split()[0][0] != "#":
real_line = line.split("#")[0]
attr_name = real_line.split("=")[0].strip()
attr_value = real_line.split("=")[1].strip()
for param_type in INCAR._type_trans.keys():
if attr_name in INCAR._type_trans[param_type]['name']:
attr_value = INCAR._type_trans[param_type]['func'](attr_value)
break
else:
raise AttributeNotRegisteredError(f"{attr_name} is not registered")
setattr(self, attr_name, attr_value)
[docs] def write(self, name):
"""
Write interface, callback every _write* func
"""
self._write_base(name)
self._write_scf(name)
self._write_hse(name)
self._write_vdw(name)
self._write_sol(name)
self._write_opt(name)
self._write_md(name)
self._write_charge(name)
self._write_wf(name)
self._write_density(name)
self._write_freq(name)
self._write_stm(name)
self._write_vtst(name)
self._write_neb(name)
self._write_dimer(name)
if self.LHFCALC is not True:
self._write_plusU(name)
@formatter(Parameter._baseParam)
def _write_base(self, name):
with open(name, "w") as f:
f.write(f"#----------/Base Parameter/----------# \n")
@formatter(Parameter._scfParam)
def _write_scf(self, name):
with open(name, "a+") as f:
f.write(f"#----------/SCF Parameter/----------# \n")
@formatter(Parameter._hseParam)
def _write_hse(self, name):
with open(name, "a+") as f:
f.write(f"#----------/HSE06 Parameter/----------# \n")
@formatter(Parameter._vdwParam)
def _write_vdw(self, name):
with open(name, "a+") as f:
f.write(f"#----------/VDW Parameter/----------# \n")
@formatter(Parameter._solParam)
def _write_sol(self, name):
with open(name, "a+") as f:
f.write(f"#----------/Solvation Parameter/----------# \n")
@formatter(Parameter._optParam)
def _write_opt(self, name):
with open(name, "a+") as f:
f.write(f"#----------/Optimize Parameter/----------# \n")
@formatter(Parameter._mdParam)
def _write_md(self, name):
with open(name, "a+") as f:
f.write(f"#----------/MD Parameter/----------# \n")
@formatter(Parameter._chgParam)
def _write_charge(self, name):
with open(name, "a+") as f:
f.write(f"#----------/Charge Parameter/----------# \n")
@formatter(Parameter._wfParam)
def _write_wf(self, name):
with open(name, "a+") as f:
f.write(f"#----------/WorkFunc Parameter/----------# \n")
@formatter(Parameter._dosParam)
def _write_density(self, name):
with open(name, "a+") as f:
f.write(f"#----------/DOS Parameter/----------# \n")
@formatter(Parameter._freqParam)
def _write_freq(self, name):
with open(name, "a+") as f:
f.write(f"#----------/Frequency Parameter/----------# \n")
@formatter(Parameter._stmParam)
def _write_stm(self, name):
with open(name, "a+") as f:
f.write(f"#----------/STM Parameter/----------# \n")
@formatter(Parameter._vtstParam)
def _write_vtst(self, name):
with open(name, "a+") as f:
f.write(f"#----------/VTST optimizer Parameter/----------# \n")
@formatter(Parameter._nebParam)
def _write_neb(self, name):
with open(name, "a+") as f:
f.write(f"#----------/NEB Parameter/----------# \n")
@formatter(Parameter._dimerParam)
def _write_dimer(self, name):
with open(name, "a+") as f:
f.write(f"#----------/Dimer Parameter/----------# \n")
@formatter(Parameter._plusUParam)
def _write_plusU(self, name):
with open(name, "a+") as f:
f.write(f"#----------/+U Parameter/----------# \n")
[docs]class KPOINTS(MetaFile):
def __init__(self, name, task='opt'):
super(KPOINTS, self).__init__(name=name)
self.task = task
self.title, self.strategy, self.center, self.number, self.weight = None, None, None, None, None
self._parse()
def _parse(self):
if self.task in ['opt']:
self.title = self.strings[0].strip()
self.strategy = self.strings[1].strip()
self.center = self.strings[2].strip()
self.number = list(map(int, self.strings[3].split()))
self.weight = list(map(float, self.strings[4].split()))
[docs] def write(self, name):
with open(name) as f:
f.write(self.title)
f.write(self.strategy)
f.write(self.center)
f.write(self.number)
f.write(self.weight)
[docs] @staticmethod
def min_number(structure: Structure, length=20.0):
lattice = structure.lattice
kpoints = np.ceil(length / lattice.length).astype(int)
structure.atoms.set_coord(lattice)
z_max = np.max(structure.atoms.cart_coord[:, 2])
z_min = np.min(structure.atoms.cart_coord[:, 2])
vacuum = lattice.length[2] - (z_max - z_min)
if (vacuum) >= 5:
kpoints[2] = 1
return kpoints
[docs]class POTCAR(MetaFile):
def __init__(self, name):
super(POTCAR, self).__init__(name=name)
self.potential, self.element, self.valence = None, None, None
if Path(self.name).exists():
self._parse()
def __add__(self, other):
potential = self.potential if isinstance(self.potential, list) else [self.potential]
element = self.element if isinstance(self.element, list) else [self.element]
valence = self.valence if isinstance(self.valence, list) else [self.valence]
strings = self._strings
potential += other.potential
element += other.element
valence += other.valence
strings += other._strings
potcar = POTCAR("POTCAR")
potcar.potential = potential
potcar.element = element
potcar.valence = valence
potcar._strings = strings
return potcar
def _parse(self):
self.potential = [line.split()[2] for line in self.strings if line.find("TITEL") != -1]
self.element = [line.split()[3] for line in self.strings if line.find("TITEL") != -1]
self.valence = [float(line.split()[5]) for line in self.strings if line.find("ZVAL") != -1]
[docs] @staticmethod
def cat(potentials, elements: List[str], potdir=f"{RootDir}/pot"):
if potdir is None or not Path(potdir).exists():
raise PotDirNotExistError(f"potdir = {potdir} is not exist (should named as `pot`), please check")
if (isinstance(potentials, str) and potentials not in POTENTIAL) or \
(isinstance(potentials, list) and len(set(potentials).difference(set(POTENTIAL)))):
raise TypeError(f"potentials should be {POTENTIAL}")
if isinstance(potentials, str):
potentials = [potentials] * len(elements)
elif isinstance(potentials, list) and len(potentials) == len(elements):
pass
else:
raise ParameterError(f"{potentials} and {elements} is not match")
potcar_objects = []
for potential, element in zip(potentials, elements):
marker = is_subset_recommend_pot(element)
if potential == "PAW_PBE" and marker:
potcar_objects.append(POTCAR(name=(Path(potdir) / potential / f"{Path(element)}_{marker}" / "POTCAR")))
logger.warning(
f"VASP recommend `{element} ({potential})` use *_{marker} potential "
f"[https://www.vasp.at/wiki/index.php/Available_PAW_potentials#Recommended_potentials_for_DFT_calculationsss]")
else:
potcar_objects.append(POTCAR(name=(Path(potdir) / potential / f"{Path(element)}" / "POTCAR")))
return reduce(add, potcar_objects)
[docs] def write(self, name):
with open(name, "w") as f:
f.writelines(self.strings)
[docs]class XSDFile(MetaFile):
def __init__(self, name):
super(XSDFile, self).__init__(name=name)
self._xml = None
self.element, self.spin, self.frac_coord, self.selective_dynamics, self.lattice = None, None, None, None, None
self.constrain = None
self._parse()
@property
def strings(self):
if self._strings is None:
with open(self.name, "r") as f:
self._strings = f.read()
return self._strings
def _parse(self):
self._xml = etree.XML(self.strings.encode("utf-8"))
try:
SpaceGroupName = self._xml.xpath("//SpaceGroup//@Name")[0] # Cu2O xsd file (why generate no reason)
except IndexError:
SpaceGroupName = self._xml.xpath("//SpaceGroup//@GroupName")[0]
if SpaceGroupName == "P1":
Atom3d = self._xml.xpath("//Atom3d[@Components]")
IdentifyMapping = self._xml.xpath("//MappingFamily//IdentityMapping")[0]
identify_coord = [float(value) for index, value in
enumerate(IdentifyMapping.attrib['Constraint'].split(","))
if (index + 1) % 4 == 0]
Components = [item.split(",")[0] for item in self._xml.xpath("//Atom3d//@Components")]
Name = [atom.attrib.get('Name', atom.attrib['Components']) for atom in Atom3d]
FormalSpin = [int(atom.attrib.get('FormalSpin', '0')) for atom in Atom3d]
XYZ = []
for atom in Atom3d:
item = atom.attrib.get('XYZ', None)
if item is not None:
XYZ.append(list(map(float, item.split(","))))
else:
XYZ.append(identify_coord)
RestrictedProperties = [atom.attrib.get('RestrictedProperties', 'T T T') for atom in Atom3d]
TF = [item.replace("FractionalXYZ", "F F F").split() for item in RestrictedProperties]
else:
Atom3d = self._xml.xpath("//MappingFamily//Atom3d")
IdentifyMapping = self._xml.xpath("//MappingFamily//Atom3d//..")
atoms = []
for atom, identify in zip(Atom3d, IdentifyMapping):
if atom.attrib.get('Components', None) is not None:
formula = atom.attrib['Components']
formal_spin = int(atom.attrib.get('FormalSpin', '0'))
name = atom.attrib.get('Name', atom.attrib['Components'])
restricted_property = atom.attrib.get('RestrictedProperties', 'T T T')
tf = restricted_property.replace("FractionalXYZ", "F F F").split()
identify_coord = [float(value) for index, value in enumerate(identify.attrib['Constraint'].split(","))
if (index + 1) % 4 == 0]
atoms.append((formula, identify_coord, formal_spin, name, tf))
identity_atoms = remove_mapping(atoms)
Components = [atom[0] for atom in identity_atoms]
XYZ = [atom[1] for atom in identity_atoms]
FormalSpin = [atom[2] for atom in identity_atoms]
Name = [atom[3] for atom in identity_atoms]
TF = [atom[4] for atom in identity_atoms]
assert len(XYZ) == len(Components) == len(Name) == len(FormalSpin) == len(TF)
constrain = [index for index, name in enumerate(Name) if name.endswith("_c")]
SpaceGroup = self._xml.xpath("//SpaceGroup")[0]
Vector = [list(map(float, SpaceGroup.attrib[key].split(","))) for key in SpaceGroup.keys() if "Vector" in key]
self.element = Components
self.constrain = constrain
self.spin = FormalSpin
self.frac_coord = XYZ
self.selective_dynamics = TF
self.lattice = Vector
@property
def structure(self):
results = sorted(zip(range(len(self.element)), self.element, self.frac_coord), key=lambda x: (x[1], x[2][2]))
sorted_order, sorted_element, sorted_frac_coord = list(zip(*results))
sorted_constrain = list(map(lambda x: True if x in self.constrain else False, sorted_order))
sorted_selective_dynamics = np.array(self.selective_dynamics)[list(sorted_order)]
sorted_spin = np.array(self.spin)[list(sorted_order)]
atoms = Atoms(formula=sorted_element, frac_coord=sorted_frac_coord, selective_matrix=sorted_selective_dynamics,
constrain=sorted_constrain, spin=sorted_spin)
lattice = Lattice(np.array(self.lattice))
return Structure(atoms=atoms, lattice=lattice)
[docs] @staticmethod
def write(contcar: Union[str, Path], outcar: Union[str, Path], name='output.xsd'):
structure = CONTCAR(contcar).structure
outcar = OUTCAR(outcar)
doc = minidom.Document()
doctype = DocumentType(qualifiedName="XSD []")
XSD = doc.createElement('XSD')
XSD.setAttribute('Version', "5.0")
XSD.setAttribute('WrittenBy', "Materials Studio 5.0")
AtomisticTreeRoot = doc.createElement('AtomisticTreeRoot')
AtomisticTreeRoot.setAttribute('ID', "1")
AtomisticTreeRoot.setAttribute('NumProperties', "58")
AtomisticTreeRoot.setAttribute('NumChildren', "1")
PropertyString = '<root>' \
'<Property Name="AngleAxisType" DefinedOn="AngleBetweenPlanesBender" Type="Enumerated"/>' \
'<Property Name="AngleEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="BeadDocumentID" DefinedOn="MesoMoleculeSet" Type="String"/>' \
'<Property Name="BendBendEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="BendTorsionBendEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="BondEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="EFGAsymmetry" DefinedOn="Atom" Type="Double"/>' \
'<Property Name="EFGQuadrupolarCoupling" DefinedOn="Atom" Type="Double"/>' \
'<Property Name="ElectrostaticEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="FaceMillerIndex" DefinedOn="GrowthFace" Type="MillerIndex"/>' \
'<Property Name="FacetTransparency" DefinedOn="GrowthFace" Type="Float"/>' \
'<Property Name="FermiLevel" DefinedOn="ScalarFieldBase" Type="Double"/>' \
'<Property Name="Force" DefinedOn="Matter" Type="CoDirection"/>' \
'<Property Name="FrameFilter" DefinedOn="Trajectory" Type="String"/>' \
'<Property Name="HarmonicForceConstant" DefinedOn="HarmonicRestraint" Type="Double"/>' \
'<Property Name="HarmonicMinimum" DefinedOn="HarmonicRestraint" Type="Double"/>' \
'<Property Name="HydrogenBondEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="ImportOrder" DefinedOn="Bondable" Type="UnsignedInteger"/>' \
'<Property Name="InversionEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="IsBackboneAtom" DefinedOn="Atom" Type="Boolean"/>' \
'<Property Name="IsChiralCenter" DefinedOn="Atom" Type="Boolean"/>' \
'<Property Name="IsOutOfPlane" DefinedOn="Atom" Type="Boolean"/>' \
'<Property Name="IsRepeatArrowVisible" DefinedOn="ElectrodeWire" Type="Boolean"/>' \
'<Property Name="KineticEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="LineExtentPadding" DefinedOn="BestFitLineMonitor" Type="Double"/>' \
'<Property Name="LinkageGroupName" DefinedOn="Linkage" Type="String"/>' \
'<Property Name="ListIdentifier" DefinedOn="PropertyList" Type="String"/>' \
'<Property Name="NMRShielding" DefinedOn="Atom" Type="Double"/>' \
'<Property Name="NonBondEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="NormalMode" DefinedOn="Bondable" Type="Direction"/>' \
'<Property Name="NormalModeFrequency" DefinedOn="Bondable" Type="Double"/>' \
'<Property Name="NumScanSteps" DefinedOn="LinearScan" Type="UnsignedInteger"/>' \
'<Property Name="OrbitalCutoffRadius" DefinedOn="Bondable" Type="Double"/>' \
'<Property Name="PlaneExtentPadding" DefinedOn="BestFitPlaneMonitor" Type="Double"/>' \
'<Property Name="PotentialEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="QuantizationValue" DefinedOn="ScalarFieldBase" Type="Double"/>' \
'<Property Name="RelativeVelocity" DefinedOn="Matter" Type="Direction"/>' \
'<Property Name="RepeatArrowScale" DefinedOn="ElectrodeWire" Type="Float"/>' \
'<Property Name="RestraintEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="ScanEnd" DefinedOn="LinearScan" Type="Double"/>' \
'<Property Name="ScanStart" DefinedOn="LinearScan" Type="Double"/>' \
'<Property Name="SeparatedStretchStretchEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="SimulationStep" DefinedOn="Trajectory" Type="Integer"/>' \
'<Property Name="StretchBendStretchEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="StretchStretchEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="StretchTorsionStretchEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="Temperature" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="ThreeBodyNonBondEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="TorsionBendBendEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="TorsionEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="TorsionStretchEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="TotalEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="Units" DefinedOn="ScalarFieldBase" Type="String"/>' \
'<Property Name="ValenceCrossTermEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="ValenceDiagonalEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="VanDerWaalsEnergy" DefinedOn="ClassicalEnergyHolder" Type="Double"/>' \
'<Property Name="_Stress" DefinedOn="MatterSymmetrySystem" Type="Matrix"/>' \
'<Property Name="_TrajectoryStress" DefinedOn="MatterSymmetrySystem" Type="Matrix"/>' \
'</root>'
Propertys = parseString(PropertyString).getElementsByTagName("Property")
for Property in Propertys:
AtomisticTreeRoot.appendChild(Property)
atoms_num = structure.atoms.count
energy = outcar.last_energy
force = outcar.last_force
mag = outcar.last_mag
SymmetrySystem = doc.createElement("SymmetrySystem")
SymmetrySystem.setAttribute("ID", "2")
SymmetrySystem.setAttribute("Mapping", "3")
SymmetrySystem.setAttribute("Children", f"ci({atoms_num + 2}):{atoms_num + 2}+4")
SymmetrySystem.setAttribute("Normalized", "1")
SymmetrySystem.setAttribute("Name", f"E:{energy:.3f} F:{force:.3f} M:{mag:.2f}")
SymmetrySystem.setAttribute("XYZ", "0,0,0")
SymmetrySystem.setAttribute("OverspecificationTolerance", "0.05")
SymmetrySystem.setAttribute("PeriodicDisplayType", "Original")
SymmetrySystem.setAttribute("HasSymmetryHistory", "1")
MappingSet = doc.createElement("MappingSet")
MappingSet.setAttribute("ID", f"{atoms_num + 6}")
MappingSet.setAttribute("SymmetryDefinition", f"{atoms_num + 4}")
MappingSet.setAttribute("ActiveSystem", "2")
MappingSet.setAttribute("NumFamilies", "1")
MappingSet.setAttribute("OwnsTotalConstraintMapping", "1")
MappingSet.setAttribute("TotalConstraintMapping", "3")
MappingFamily = doc.createElement("MappingFamily")
MappingFamily.setAttribute("ID", f"{atoms_num + 7}")
MappingFamily.setAttribute("NumImageMappings", "0")
IdentityMapping = doc.createElement("IdentityMapping")
IdentityMapping.setAttribute("ID", f"{atoms_num + 8}")
IdentityMapping.setAttribute("Element", "1,0,0,0,0,1,0,0,0,0,1,0")
IdentityMapping.setAttribute("Constraint", "1,0,0,0,0,1,0,0,0,0,1,0")
IdentityMapping.setAttribute("MappedObjects", f"ci({atoms_num + 1}):{atoms_num}+4,{atoms_num + 5}")
IdentityMapping.setAttribute("DefectObjects", f"{atoms_num + 4},{atoms_num + 9}")
IdentityMapping.setAttribute("NumImages", f"{atoms_num + 1}")
IdentityMapping.setAttribute("NumDefects", "2")
for atom in structure.atoms:
Atom3d = doc.createElement("Atom3d")
Atom3d.setAttribute("ID", f"{atom.order + 4}")
Atom3d.setAttribute("Mapping", f"{atoms_num + 8}")
Atom3d.setAttribute("Parent", "2")
if (atom.selective_matrix == ['F', 'F', 'F']).all():
Atom3d.setAttribute("RestrictedBy", f"{atoms_num + 5}")
Atom3d.setAttribute("RestrictedProperties", "FractionalXYZ")
Atom3d.setAttribute("Parent", "2")
Atom3d.setAttribute("Name", f"{atom.formula}{atom.order + 1}")
Atom3d.setAttribute("XYZ", f"{atom.frac_coord[0]:.12f},{atom.frac_coord[1]:.12f},{atom.frac_coord[2]:.12f}")
Atom3d.setAttribute("Charge", "0")
Atom3d.setAttribute("Components", f"{atom.formula}")
Atom3d.setAttribute("FormalSpin", "0")
IdentityMapping.appendChild(Atom3d)
CompleteRestriction = doc.createElement("CompleteRestriction")
CompleteRestriction.setAttribute("ID", f"{atoms_num + 5}")
CompleteRestriction.setAttribute("Mapping", f"{atoms_num + 8}")
CompleteRestriction.setAttribute("Parent", "2")
fix_atoms = [atom.order + 4 for atom in structure.atoms if (atom.selective_matrix == ['F', 'F', 'F']).all()]
if len(fix_atoms):
CompleteRestriction.setAttribute("RestrictsObjects",
f"ci({len(fix_atoms)}):{','.join([str(item) for item in fix_atoms])}")
CompleteRestriction.setAttribute("RestrictsProperties",
f"{','.join(['FractionalXYZ'] * len(fix_atoms))}")
else:
CompleteRestriction.setAttribute("Visible", "0")
matrix = structure.lattice.matrix
SpaceGroup = doc.createElement("SpaceGroup")
SpaceGroup.setAttribute("ID", f"{atoms_num + 4}")
SpaceGroup.setAttribute("Parent", "2")
SpaceGroup.setAttribute("Children", f"{atoms_num + 9}")
SpaceGroup.setAttribute("Name", "P1")
SpaceGroup.setAttribute("AVector", f"{matrix[0, 0]:.10f},{matrix[0, 1]:.10f},{matrix[0, 2]:.10f}")
SpaceGroup.setAttribute("BVector", f"{matrix[1, 0]:.10f},{matrix[1, 1]:.10f},{matrix[1, 2]:.10f}")
SpaceGroup.setAttribute("CVector", f"{matrix[2, 0]:.10f},{matrix[2, 1]:.10f},{matrix[2, 2]:.10f}")
SpaceGroup.setAttribute("Color", "136,0,204,255")
SpaceGroup.setAttribute("OrientationBase", "C along Z, A in XZ plane")
SpaceGroup.setAttribute("Centering", "3D Primitive-Centered")
SpaceGroup.setAttribute("Lattice", "3D Triclinic")
SpaceGroup.setAttribute("GroupName", "P1")
SpaceGroup.setAttribute("Operators", "1,0,0,0,0,1,0,0,0,0,1,0")
SpaceGroup.setAttribute("DisplayRange", "0,1,0,1,0,1")
SpaceGroup.setAttribute("CylinderRadius", "0.2")
SpaceGroup.setAttribute("LabelAxes", "1")
SpaceGroup.setAttribute("ActiveSystem", "2")
SpaceGroup.setAttribute("ITNumber", "1")
SpaceGroup.setAttribute("LongName", "P 1")
SpaceGroup.setAttribute("Qualifier", "Origin-1")
SpaceGroup.setAttribute("SchoenfliesName", "C1-1")
SpaceGroup.setAttribute("System", "Triclinic")
SpaceGroup.setAttribute("Class", "1")
SpaceGroup.setAttribute("DisplayStyle", "Dashed")
SpaceGroup.setAttribute("LineThickness", "4")
ReciprocalLattice3D = doc.createElement("ReciprocalLattice3D")
ReciprocalLattice3D.setAttribute("ID", f"{atoms_num + 9}")
ReciprocalLattice3D.setAttribute("Parent", f"{atoms_num + 4}")
ReciprocalLattice3D.setAttribute("Visible", "0")
IdentityMapping.appendChild(CompleteRestriction)
IdentityMapping.appendChild(SpaceGroup)
IdentityMapping.appendChild(ReciprocalLattice3D)
MappingRepairs = doc.createElement("MappingRepairs")
MappingRepairs.setAttribute("NumRepairs", "0")
MappingFamily.appendChild(IdentityMapping)
MappingFamily.appendChild(MappingRepairs)
InfiniteMapping = doc.createElement("InfiniteMapping")
InfiniteMapping.setAttribute("ID", "3")
InfiniteMapping.setAttribute("Element", "1,0,0,0,0,1,0,0,0,0,1,0")
InfiniteMapping.setAttribute("MappedObjects", "2")
MappingSet.appendChild(MappingFamily)
MappingSet.appendChild(InfiniteMapping)
OriginalObjects = doc.createElement("OriginalObjects")
OriginalObjects.setAttribute("ID", f"{atoms_num + 10}")
SetCollection = doc.createElement("SetCollection")
SetCollection.setAttribute("Objects", f"ci({atoms_num}):{atoms_num}+4")
OriginalObjects.appendChild(SetCollection)
SymmetrySystem.appendChild(MappingSet)
SymmetrySystem.appendChild(OriginalObjects)
AtomisticTreeRoot.appendChild(SymmetrySystem)
XSD.appendChild(AtomisticTreeRoot)
doc.appendChild(doctype)
doc.appendChild(XSD)
stem = Path(name).stem
stem = stem + "-y" if outcar.finish else stem + "-n"
with open(f"{stem}.xsd", "w") as f:
doc.writexml(f, indent='\t', addindent='\t', newl='\n', encoding="latin1")
logger.info(f"{RED}transform to {stem}.xsd file{RESET}")
[docs]class POSCAR(StructInfoFile):
[docs] @staticmethod
def dist(pos1: str, pos2: str):
"""
Calculate the distance of two POSCAR, distance = sqrt(sum((i-j)**2))
@param:
pos1: first POSCAR file name
pos2: second POSCAR file name
"""
structure1 = POSCAR(name=pos1).structure
structure2 = POSCAR(name=pos2).structure
return Structure.dist(structure1, structure2)
[docs] @staticmethod
def align(pos1: str, pos2: str):
"""
Tailor the atoms' order to make the distance of pos1 and pos2 minimum
@param:
pos1: first POSCAR file name
pos2: second POSCAR file name
"""
structure1 = POSCAR(name=pos1).structure
structure2 = POSCAR(name=pos2).structure
logger.info(f"Align before: dist = {Structure.dist(structure1, structure2)}")
structure1_new, structure2_new = Structure.align(structure1, structure2)
logger.info(f"Align before: dist = {Structure.dist(structure1_new, structure2_new)}")
structure1_new.write_POSCAR(f"{pos1}_sort")
structure2_new.write_POSCAR(f"{pos2}_sort")
logger.info(f"New structure have been written to *_sort files")
[docs]class CONTCAR(POSCAR):
pass
[docs]class XDATCAR(StructInfoFile):
def __init__(self, name):
super().__init__(name)
self.lattice = Lattice.from_string(self.strings[2:5])
element_name = self.strings[5].split()
element_count = [int(item) for item in self.strings[6].split()]
self.element = sum([[name] * count for name, count in zip(element_name, element_count)], [])
self.frames = [i for i in range(len(self.strings)) if self.strings[i].find("Direct") != -1]
self._structure = []
@property
def structure(self): # overwrite <structure method>
if len(self._structure) == 0:
for frame in self.frames:
frac_coord = np.array([[float(item) for item in line.split()] for line in
self.strings[frame + 1:frame + 1 + len(self.element)]])
atoms = Atoms(formula=self.element, frac_coord=frac_coord)
self._structure.append(Structure(atoms=atoms, lattice=self.lattice))
return self._structure
[docs] def movie(self, name):
"""Transform the XDATCAR to arc file"""
ARCFile.write(name=name, structure=self.structure, lattice=self.lattice)
[docs]class DOSCAR(MetaFile):
def __init__(self, name, LORBIT=12):
super(DOSCAR, self).__init__(name=name)
self.LORBIT = LORBIT
self.NAtom = int(self.strings[0].split()[0])
self.Emax, self.Emin, self.NDOS, self.fermi = tuple(map(float, self.strings[5].split()[:4]))
self.NDOS = int(self.NDOS)
[docs] def load(self):
def merge_dos(energy_list, Total_up, Total_down, atom_list, length):
"""TODO: need optimize, deprecate DataFrame"""
atom_data = [energy_list]
Total_Dos = DataFrame(index=energy_list, columns=['tot_up', 'tot_down'], dtype='object')
Total_Dos['tot_up'] = Total_up
Total_Dos['tot_down'] = Total_down
if self.LORBIT == 10:
columns = COLUMNS_8[:length]
orbitals = []
elif self.LORBIT == 12:
columns = COLUMNS_32[:length]
orbitals = ORBITALS[1:int(math.sqrt(length / 2))]
else:
logger.error(f"LORBIT = {self.LORBIT} is not supported in this version!")
exit(1)
for data in atom_list:
DATA = DataFrame(data, index=energy_list, columns=columns)
DATA['up'] = 0.0
DATA['down'] = 0.0
# LORBIT = 12, sum projected-orbitals, e.g., px+py+pz = p
for orbital in orbitals:
DATA[orbital + '_up'] = 0.0
DATA[orbital + '_down'] = 0.0
orbital_p_up = [item for item in DATA.columns.values if
item.startswith(orbital) and item.endswith('up') and item != f'{orbital}_up'
and item != 'up']
orbital_p_down = [item for item in DATA.columns.values if
item.startswith(orbital) and item.endswith('down') and item != f'{orbital}_down'
and item != 'down']
for item in orbital_p_up:
DATA[f'{orbital}_up'] += DATA[item]
for item in orbital_p_down:
DATA[f'{orbital}_down'] += DATA[item]
DATA['up'] += DATA[f'{orbital}_up']
DATA['down'] += DATA[f'{orbital}_down']
# for both LORBIT=10/12
DATA['up'] += DATA['s_up']
DATA['down'] += DATA['s_down']
# LORBIT = 10
if self.LORBIT == 10:
for item in columns:
if item.endswith("up") and not item.startswith("s"):
DATA['up'] += DATA[item]
elif item.endswith("down") and not item.startswith("s"):
DATA['down'] += DATA[item]
atom_data.append(DATA)
self.TDOS = Total_Dos # DataFrame(NDOS, 2)
self.LDOS = atom_data # energy + List(NAtom, NDOS, NOrbital+8)
return self
return merge_dos(*dos_cython.load(self.strings, self.NDOS, self.fermi))
[docs]class EIGENVAL(MetaFile):
def __init__(self, name):
super(EIGENVAL, self).__init__(name=name)
self.NKPoint, self.NBand = tuple(map(int, self.strings[5].split()[1:]))
self.KPoint_coord = None
self.KPoint_dist = None
self.KPoint_label = None
self.energy = None
self._parse()
def _parse(self):
"""
load Eigenval obtain the band-energy
@return:
self.energy: shape=(NKPoint, NBand, 2)
"""
self.KPoint_coord = []
self.energy = []
energy_flag = False
sBand = []
for index, line in enumerate(self.strings):
if index % (self.NBand + 2) == 7:
self.KPoint_coord.append(tuple(map(float, line.split()[0:3])))
energy_flag = True
continue
elif energy_flag:
sBand.append(list(map(float, line.split()[1:3])))
if len(sBand) == self.NBand:
energy_flag = False
self.energy.append(sBand)
sBand = []
self.KPoint_coord = np.array(self.KPoint_coord)
self.KPoint_dist = []
self.KPoint_label = []
for index, k_point in enumerate(self.KPoint_coord):
if index == 0:
self.KPoint_dist.append(0.)
else:
k_before = self.KPoint_coord[index - 1]
dist = np.sum((k_point - k_before) ** 2) ** 0.5
self.KPoint_dist.append(self.KPoint_dist[-1] + dist)
for label, value in HIGH_SYM.items():
if np.sum((np.array(value) - k_point) ** 2) ** 0.5 <= 1E-02 and (
not len(self.KPoint_label) or self.KPoint_label[-1] != label):
self.KPoint_label.append(label)
break
else:
self.KPoint_label.append("")
self.energy = np.array(self.energy)
return self
[docs] def write(self, directory='band_data'):
"""
Write band-data to file, each band corresponding to one file and named as band_{index}
@params:
dir: save directory, default: $CWD/band_data
"""
Path(directory).mkdir(exist_ok=True)
for index in range(self.NBand):
np.savetxt(f"{directory}/band_{index + 1}", self.energy[:, index])
logger.info(f"Band data has been saved to {directory} directory")
[docs]class CHGBase(StructInfoFile):
"""
Subclass of StructInfoFile, inherit <structure property>
"""
def __new__(cls, *args, **kwargs):
if cls is CHGBase:
raise TypeError(f"<{cls.__name__} class> may not be instantiated")
return super().__new__(cls)
def __init__(self, name):
super(CHGBase, self).__init__(name=name)
self.NGX, self.NGY, self.NGZ = None, None, None
self.density = None
def __add__(self, other):
if self.__class__.__name__.startswith("AECCAR") and other.__class__.__name__.startswith("AECCAR"):
if self.density is None and other.density is None:
pool = ProcessPool(processes=2)
results = [pool.apply_async(self.load), pool.apply_async(other.load)] # new instance
self, other = [item.get() for item in results]
pool.close()
pool.join()
elif self.density is None:
self.load()
elif other.density is None:
other.load()
if self.structure != other.structure:
raise StructureNotEqualError(f"{self.name}.structure is not equal to {other.name}.structure")
if (self.NGX, self.NGY, self.NGZ) != (other.NGX, other.NGY, other.NGZ):
raise GridNotEqualError(f"{self.name}.NGrid is not equal to {other.name}.NGrid")
density_sum = self.density + other.density
return CHGCAR_sum.from_array("CHGCAR_sum", self.structure, (self.NGX, self.NGY, self.NGZ), density_sum)
else:
raise TypeError(
f"unsupported operand type(s) for +: {self.__class__.__name__} and {other.__class__.__name__}")
[docs] def load(self):
"""
load Electronic-Density
@return:
self.density: shape=(NGX, NGY, NGZ)
"""
info = file_bind.load(self.name)
self.NGX, self.NGY, self.NGZ = info.NGX, info.NGY, info.NGZ
self.density = info.density
assert self.density.size == self.NGX * self.NGY * self.NGZ, "Load density failure, size is not consistent"
self.density = self.density.reshape((self.NGX, self.NGY, self.NGZ), order="F")
return self
[docs] def write(self, title=None, factor=1.0):
"""
write CHGCAR_* file from array
@param:
system: specify the structure system
factor: coordination factor
"""
self.structure.write_POSCAR(name=self.__class__.__name__, title=title, factor=factor)
density_fortran = self.density.reshape(-1, order="F")
# solve residue problem
residue = density_fortran.size % 5
if residue:
density_fortran1 = density_fortran[:-residue].reshape(-1, 5)
density_fortran2 = density_fortran[-residue:].reshape(1, -1)
else:
density_fortran1 = density_fortran.reshape(-1, 5)
density_fortran2 = None
with open(self.__class__.__name__, "a+") as f:
f.write(f"{self.NGX:>5}{self.NGY:>5}{self.NGZ:>5}\n")
np.savetxt(f, density_fortran1, fmt="%18.11E")
if density_fortran2 is not None:
np.savetxt(f, density_fortran2, fmt="%18.11E")
[docs]class AECCAR0(CHGBase):
pass
[docs]class AECCAR2(CHGBase):
pass
[docs]class CHGCAR_sum(CHGBase):
def __init__(self, name):
super(CHGCAR_sum, self).__init__(name=name)
self._structure = None
@property
def structure(self):
if self._structure is None:
self._structure = super(CHGCAR_sum, self).structure()
return self._structure
@structure.setter
def structure(self, _structure):
self._structure = _structure
[docs] @staticmethod
def from_array(name: str, structure, NGrid: tuple[int, int, int], density):
instance = CHGCAR_sum(name=name)
instance.structure = structure
instance.NGX, instance.NGY, instance.NGZ = NGrid
instance.density = density
return instance
[docs]class CHGCAR_tot(CHGBase):
pass
[docs]class CHGCAR_mag(CHGBase):
[docs] @staticmethod
def to_grd(name="vasp.grd", DenCut=-1):
"""
transform CHGCAR_mag to grd file
param:
name: specify the name of grd file
DenCut: density lower than DenCut will be set to zero (default: -1: disable the DenCut option)
"""
file_bind.to_grd(name, DenCut)
[docs]class CHGCAR_diff(CHGBase):
[docs] def line_potential(self, direction='z'):
"""
Calculate Charge Density Difference along one direction, default: z-axis
Args:
direction (str): which axis you want to Average the CCD
Returns:
line_potential (np.array[:]): CCD along one axis
"""
mapping = {'x': 0, 'y': 1, 'z': 2}
if getattr(self, 'density', None) is None:
self.load()
NGXYZ = (self.NGX, self.NGY, self.NGZ)
if mapping.get(direction, None) is None:
raise KeyError(f"{direction} is not supported, should be [x, y, z]")
potential_swap = np.swapaxes(self.density / self.structure.lattice.length[mapping[direction]], -1,
mapping[direction])
return np.linspace(start=0, stop=self.structure.lattice.length[mapping[direction]],
num=NGXYZ[mapping[direction]]), np.mean(potential_swap, axis=(0, 1))
[docs]class CHGCAR(StructInfoFile):
def __init__(self, name):
super(CHGCAR, self).__init__(name=name)
self.NGX, self.NGY, self.NGZ, self.NGrid = None, None, None, None
self.density_tot, self.density_mag = None, None
self._head = None
self._density_tot_strings, self._density_mag_strings = None, None
[docs] def load(self):
"""
load Electronic-Density
@return:
self.NGrid: NGX * NGY * NGZ
self.density: shape=(NGX, NGY, NGZ)
self._density_strings: density with strings format
"""
start = len(self.structure.atoms) + 9
self.NGX, self.NGY, self.NGZ = tuple(map(int, self.strings[start].split()))
self._head = self.strings[:start + 1]
index = np.where(np.array(self.strings) == self.strings[start])[0]
assert len(index) == 2, f"Search indicator failure"
self.NGrid = self.NGX * self.NGY * self.NGZ
count = self.NGrid / 5 if self.NGrid % 5 == 0 else self.NGrid / 5 + 1
count = int(count)
self._density_tot_strings = self.strings[index[0] + 1:index[0] + 1 + count]
self._density_mag_strings = self.strings[index[1] + 1:index[1] + 1 + count]
# solve residue problem
self._density_tot_strings_list = np.char.split(self._density_tot_strings).tolist()
residue = 5 - len(self._density_tot_strings_list[-1])
self._density_tot_strings_list[-1] += ['0.'] * residue
self._density_mag_strings_list = np.char.split(self._density_mag_strings).tolist()
self._density_mag_strings_list[-1] += ['0.'] * residue
self.density_tot = np.append([], self._density_tot_strings_list).astype(float)
self.density_tot = self.density_tot[:-residue].reshape((self.NGX, self.NGY, self.NGZ), order="F")
self.density_mag = np.append([], self._density_mag_strings_list).astype(float)
self.density_mag = self.density_mag[:-residue].reshape((self.NGX, self.NGY, self.NGZ), order="F")
[docs] def split(self):
"""split CHGCAR to CHGCAR_tot && CHGCAR_mag"""
if getattr(self, "_head", None) is None:
self.load()
with open("CHGCAR_tot", "w") as tot, open("CHGCAR_mag", "w") as mag:
tot.writelines(self._head)
tot.writelines(self._density_tot_strings)
mag.writelines(self._head)
mag.writelines(self._density_mag_strings)
[docs]class ACFFile(MetaFile):
def __init__(self, name):
super(ACFFile, self).__init__(name=name)
self.cart_coord = []
self.charge = []
self._parse()
def _parse(self):
for line in self.strings[2:-4]:
item = line.split()
self.cart_coord.append(list(map(float, item[1:4])))
self.charge.append(float(item[4]))
self.cart_coord = np.array(self.cart_coord)
self.charge = np.array(self.charge)
[docs]class LOCPOT(StructInfoFile):
def __init__(self, name):
super(LOCPOT, self).__init__(name=name)
self.NGX, self.NGY, self.NGZ, self.NGrid = None, None, None, None
self.potential = None
self.lattice = None
self._head = None
self._potential_strings = None
[docs] def load(self):
"""
load Electrostatic Potential
Returns:
self.NGrid (int): value = NGX * NGY * NGZ
self.potential (np.array[:, :, :]): record the electrostatic potential
self.lattice (Lattice): <Lattice class> instance
"""
start = len(self.structure.atoms) + 9
self.NGX, self.NGY, self.NGZ = tuple(map(int, self.strings[start].split()))
self._head = self.strings[:start + 1]
self.lattice = Lattice.from_string(self._head[2:5])
index = np.where(np.array(self.strings) == self.strings[start])[0]
self.NGrid = self.NGX * self.NGY * self.NGZ
count = self.NGrid / 5 if self.NGrid % 5 == 0 else self.NGrid / 5 + 1
count = int(count)
self._potential_strings = self.strings[index[0] + 1:index[0] + 1 + count]
# solve residue problem
self._potential_strings = np.char.split(self._potential_strings).tolist()
residue = 5 - len(self._potential_strings[-1])
self._potential_strings[-1] += ['0.'] * residue
self.potential = np.append([], self._potential_strings).astype(float)
if residue:
self.potential = self.potential[:-residue].reshape((self.NGX, self.NGY, self.NGZ), order="F")
else:
self.potential = self.potential.reshape((self.NGX, self.NGY, self.NGZ), order="F")
[docs] def line_potential(self, direction='z'):
"""
Calculate the electrostatic potential along one direction, default: z-axis
Args:
direction (str): which axis you want to calculate the electrostatic potential
Returns:
line_potential (np.array[:]): electrostatic potential along one axis
"""
mapping = {'x': 0, 'y': 1, 'z': 2}
if getattr(self, 'potential', None) is None:
self.load()
NGXYZ = (self.NGX, self.NGY, self.NGZ)
if mapping.get(direction, None) is None:
raise KeyError(f"{direction} is not supported, should be [x, y, z]")
potential_swap = np.swapaxes(self.potential, -1, mapping[direction])
return np.linspace(start=0, stop=self.lattice.length[mapping[direction]],
num=NGXYZ[mapping[direction]]), np.mean(potential_swap, axis=(0, 1))
[docs]class OUTCAR(MetaFile):
def __init__(self, name):
super(OUTCAR, self).__init__(name=name)
element_name = [line.split()[3] for line in self.strings if "TITEL" in line]
element_count = [list(map(int, line.split()[4:])) for line in self.strings if "ions per" in line][0]
self.element = sum([[name] * count for name, count in zip(element_name, element_count)], [])
self.lattice = {Lattice.from_string(self.strings[index + 1:index + 4]) for index, line in
enumerate(self.strings) if "direct lattice vectors" in line}
self.lattice = list(self.lattice)[0] if len(self.lattice) == 1 else self.lattice
self._frequency = [i for i in range(len(self.strings)) if "Hz" in self.strings[i]]
self._neb = [line for line in self.strings if "NEB:" in line]
self.spin, self.bands, self.kpoints, self.fermi, self.steps = None, None, None, None, None
self.energy, self.force, self.mag = None, None, None
self.last_energy, self.last_force, self.last_mag = None, None, None
self.finish = "reached" in self.strings[-30]
self._parse_base()
self.frequency = None
if len(self._frequency):
self._parse_freq()
self.kpoint_info, self.band_info = None, None
if self.finish:
self._parse_band()
self.tangent, self.last_tangent = 0., 0.
if len(self._neb):
self._parse_neb()
def _parse_base(self):
self.spin = [int(line.split()[2]) for line in self.strings if "ISPIN" in line][0]
self.bands, self.kpoints = \
[(int(line.split()[-1]), int(line.split()[3])) for line in self.strings if "NBANDS" in line][0]
steps = [(index, int(line.split()[2].split("(")[0]), int(line.split()[3].split(")")[0]))
for index, line in enumerate(self.strings) if "Iteration" in line]
self.steps = namedtuple("Steps", ("index", "ionic", "electronic"))(*list(map(tuple, zip(*steps))))
self.fermi = [float(line.split()[2]) for line in self.strings if "E-fermi" in line][-1]
self.energy = [float(line.split()[3]) for line in self.strings if "energy without" in line]
self.force = [float(line.split()[5]) for line in self.strings if "FORCES: max atom" in line]
if self.spin == 2:
self.mag = [float(line.split()[5]) for line in self.strings if "number of electron " in line]
else:
self.mag = []
self.last_energy = self.energy[-1] if len(self.energy) else None
self.last_force = self.force[-1] if len(self.force) else None
self.last_mag = self.mag[-1] if len(self.mag) else None
def _parse_freq(self):
"""
Parse frequency information from OUTCAR
@return:
register self.frequency attr (type: namedtuple)
"""
image, wave_number, vib_energy, coord, vibration = [], [], [], [], []
for index in self._frequency:
item = self.strings[index].split()
image.append(False) if item[1] == "f" else image.append(True)
wave_number.append(float(item[-4]))
vib_energy.append(float(item[-2]))
item = list(map(lambda x: [float(i) for i in x],
np.char.split(self.strings[index + 2:index + 2 + len(self.element)])))
coord.append(np.array(item)[:, :3])
vibration.append(np.array(item)[:, 3:])
self.frequency = namedtuple("Frequency",
("image", "wave_number", "vib_energy", "coord", "vibration"))(image,
np.array(wave_number),
np.array(vib_energy),
np.array(coord),
np.array(vibration))
def _parse_band(self):
"""
Parse band information from OUTCAR
@return:
register self.kpoint_info (type: tuple(List[KPoint(coord, value)], List[KPoint(coord, value)]))
"""
def spin_obtain(kpoint_group):
"""
According to the index-list of kpoint in OUTCAR, parse the band_info
@param:
kpoint_group: list, record the index of 'k-point' field occurrence in OUTCAR
@return:
spin: List[KPoint(coord, value)], record the each k-point info
"""
spin = []
trans_func = lambda x: [int(x[0]), float(x[1]) - self.fermi, float(x[2])] # format transform
# parse band_info
for index in kpoint_group:
coord = list(map(float, band_info[index].split()[3:]))
value = list(map(trans_func, np.char.split(band_info[index + 2:index + 2 + self.bands])))
spin.append(KPoint(coord, np.array(value)))
return spin
def transform_band(spin):
band = []
for kpoint in spin:
band.append(kpoint.value[:, 1])
band = np.array(band)
band = band.transpose((1, 0))
return band
content = self.strings[self.steps.index[-1]:] # calculate bandgap from last step
start_index = [index for index, line in enumerate(content) if "E-fermi" in line][0]
end_index = [index for index, line in enumerate(content) if "-----" in line and index > start_index][0]
band_info = content[start_index:end_index] # band_info content in last step
KPoint = namedtuple("KPoint", ("coord", "value")) # including `kpoint coord` and `each band energy`
if self.spin == 2:
kpoint_index = [index for index, line in enumerate(band_info) if "k-point" in line]
half_len = int(len(kpoint_index) / 2)
kpoint_up, kpoint_down = kpoint_index[:half_len], kpoint_index[half_len:] # 'k-point' occurrence index
spin_up = spin_obtain(kpoint_up)
spin_down = spin_obtain(kpoint_down)
self.kpoint_info = namedtuple("KPoint_info", ("up", "down"))(spin_up, spin_down)
self.band_info = namedtuple("Band_info", ("up", "down"))(transform_band(spin_up), transform_band(spin_down))
else:
raise NotImplementedError("Non-spin polarized calculation has not been implemented")
def _parse_neb(self):
self.tangent = [float(line.split()[-1]) for line in self.strings if line.find("tangent") != -1]
self.last_tangent = self.tangent[-1]
[docs] def bandgap(self, cutoff=0.01):
"""
Calculated the bandgap from OUTCAR file
@param:
cutoff: any occupy lower than cutoff will be treated as the empty state
@return:
type: type of bandgap, ['direct', 'indirect']
bandgap: value of bandgap
"""
def MO_obtain(spin):
"""
According to the kpoint_info, return the homo and lumo
@param:
spin: List[KPoint(coord, value)], record the each k-point info
@return:
homo_spin: KPoint(coord, value), record the homo information
lumo_spin: KPoint(coord, value), record the lumo information
"""
# calculate homo and lumo
homo_spin, lumo_spin = None, None
for item in spin:
lumo_index = np.where(item.value[:, 2] <= cutoff)[0][0]
homo_index = lumo_index - 1
homo, lumo = item.value[homo_index], item.value[lumo_index]
homo_spin = KPoint(item.coord, homo) if homo_spin is None or homo_spin.value[1] < homo[1] else homo_spin
lumo_spin = KPoint(item.coord, lumo) if lumo_spin is None or lumo_spin.value[1] > lumo[1] else lumo_spin
return homo_spin, lumo_spin
KPoint = namedtuple("KPoint", ("coord", "value")) # including `kpoint coord` and `each band energy`
if self.spin == 2:
homo_up, lumo_up = MO_obtain(self.kpoint_info[0])
homo_down, lumo_down = MO_obtain(self.kpoint_info[1])
homo_real = homo_up if homo_up.value[1] > homo_down.value[1] else homo_down
lumo_real = lumo_up if lumo_up.value[1] < lumo_down.value[1] else lumo_down
if homo_real.coord == lumo_real.coord:
return "direct", lumo_real.value[1] - homo_real.value[1]
else:
return "indirect", lumo_real.value[1] - homo_real.value[1]
else:
raise NotImplementedError("Non-spin polarized calculation has not been implemented")
[docs] def animation_freq(self, freq: [str, int] = "image", frames: int = 30, scale: float = 0.6):
"""
Generate freq.arc file from OUTCAR
@param:
freq: specify which freq you want to animate, accept [int, str] arguments
frames: specify how many points you want to interpolate along one direction, default = 30
scale: determine the vibration scale, default = 0.6
"""
if self.frequency is None:
raise AnimationError(f"{self.name} don't include frequency information")
if isinstance(freq, int) and freq not in range(len(self._frequency)):
raise FrequencyError(f"freq{freq} is not in {self.name}, should be {list(range(len(self._frequency)))}")
if isinstance(freq, str) and freq != "image":
raise FrequencyError(f"`{freq}` is not supported, should be `image`")
freq = [index for index, item in enumerate(self.frequency.image) if item] if isinstance(freq, str) else [freq]
if len(freq) == 0:
raise FrequencyError(f"`{freq}` frequency is not found")
# generate the directions for vibration
direction_001 = np.linspace(start=0, stop=scale, num=frames) # 0-1 direction
direction_010 = np.linspace(start=scale, stop=0, num=frames) # 1-0 direction
direction_101 = np.linspace(start=0, stop=-scale, num=frames) # 0-^1 direction
direction_110 = np.linspace(start=-scale, stop=0, num=frames) # ^1-0 direction
direction_all = np.concatenate([direction_001, direction_010, direction_101, direction_110])
direction_all = direction_all[:, np.newaxis, np.newaxis]
# generate multi-frames new coordinates
coord_freq = []
for freq_index in freq:
logger.info(f"Processing freq{freq_index + 1} file ...")
coord = np.repeat(self.frequency.coord[freq_index][np.newaxis, :], direction_all.shape[0], axis=0)
vibration = np.repeat(self.frequency.vibration[freq_index][np.newaxis, :], direction_all.shape[0], axis=0)
coord_freq.append(coord + vibration * direction_all)
# generate the *.arc file
if not isinstance(self.lattice, Lattice):
raise NotImplementedError("we here only considered the lattice unchangeable for the whole calculation")
for freq_index, coord_index in zip(freq, coord_freq):
structure = [Structure(atoms=Atoms(formula=self.element, cart_coord=cart_coord), lattice=self.lattice)
for cart_coord in coord_index]
ARCFile.write(name=f"freq{freq_index + 1}.arc", structure=structure, lattice=self.lattice)
logger.info(f"All freq transform to corresponding *.arc files")
[docs]class MODECAR(MetaFile):
[docs] @staticmethod
def write_from_freq(freq: int, scale: float, outcar="OUTCAR"):
"""
Generate MODECAR file from the `image-freq`
@param:
freq: which freq you want to generate MODECAR
scale: scale factor, may lower than 1.0
outcar: name of the OUTCAR
"""
outcar = OUTCAR(name=outcar)
frequency = outcar.frequency.vibration[freq]
np.savetxt("MODECAR", frequency * scale, fmt="%10.5f")
[docs] @staticmethod
def write_from_POSCAR(pos1: str, pos2: str):
"""
Generate MODECAR file from the two `POSCAR` file
@param:
pos1: name of first POSCAR file
pos2: name of second POSCAR file
"""
structure1 = POSCAR(pos1).structure
structure2 = POSCAR(pos2).structure
assert structure1 == structure2, f"{pos1} and {pos2} are not structure match"
diff = structure1 - structure2
dist = Structure.dist(structure1, structure2)
diff_norm = diff / dist
np.savetxt("MODECAR", diff_norm, fmt="%20.10E")