676 lines
26 KiB
Python
676 lines
26 KiB
Python
from __future__ import annotations
|
|
import numpy as np
|
|
from typing import Union, List, Iterable
|
|
from monty.re import regrep
|
|
from echem.core.structure import Structure
|
|
from ..io_data.universal import Cube
|
|
from echem.core.electronic_structure import EBS
|
|
from echem.core.ionic_dynamics import IonicDynamics
|
|
from echem.core.constants import Angstrom2Bohr
|
|
from . import jdftx
|
|
from pymatgen.io.vasp import Procar as Procar_pmg
|
|
from nptyping import NDArray, Shape, Number
|
|
from pathlib import Path
|
|
import warnings
|
|
|
|
|
|
class Poscar:
|
|
"""Class that reads VASP POSCAR files"""
|
|
def __init__(self,
|
|
structure: Structure,
|
|
comment: str = None,
|
|
sdynamics_data: list = None):
|
|
"""
|
|
Create a Poscar instance
|
|
Args:
|
|
structure (Structure class): a base class that contains lattice, coords and species information
|
|
comment (str): a VASP comment
|
|
sdynamics_data (list, 2D np.array): data about selective dynamics for each atom. [['T', 'T', 'F'],
|
|
['F', 'F', 'F'],...]
|
|
"""
|
|
self.structure = structure
|
|
self.comment = comment
|
|
self.sdynamics_data = sdynamics_data
|
|
|
|
def __repr__(self):
|
|
return f'{self.comment}\n' + repr(self.structure)
|
|
|
|
@staticmethod
|
|
def from_file(filepath: str | Path):
|
|
"""
|
|
Static method to read a POSCAR file
|
|
Args:
|
|
filepath: path to the POSCAR file
|
|
|
|
Returns:
|
|
Poscar class object
|
|
"""
|
|
if isinstance(filepath, str):
|
|
filepath = Path(filepath)
|
|
|
|
file = open(filepath, 'r')
|
|
data = file.readlines()
|
|
file.close()
|
|
|
|
comment = data[0].strip()
|
|
scale = float(data[1])
|
|
lattice = np.array([[float(i) for i in line.split()] for line in data[2:5]])
|
|
if scale < 0:
|
|
# In VASP, a negative scale factor is treated as a volume.
|
|
# We need to translate this to a proper lattice vector scaling.
|
|
vol = abs(np.linalg.det(lattice))
|
|
lattice *= (-scale / vol) ** (1 / 3)
|
|
else:
|
|
lattice *= scale
|
|
|
|
name_species = data[5].split()
|
|
num_species = [int(i) for i in data[6].split()]
|
|
species = []
|
|
for name, num in zip(name_species, num_species):
|
|
species += [name]*num
|
|
|
|
sdynamics_is_used = False
|
|
start_atoms = 8
|
|
if data[7][0] in 'sS':
|
|
sdynamics_is_used = True
|
|
start_atoms = 9
|
|
|
|
coords_are_cartesian = False
|
|
if sdynamics_is_used:
|
|
if data[8][0] in 'cCkK':
|
|
coords_are_cartesian = True
|
|
else:
|
|
if data[7][0] in 'cCkK':
|
|
coords_are_cartesian = True
|
|
|
|
coords = []
|
|
coords_scale = scale if coords_are_cartesian else 1
|
|
sdynamics_data = list() if sdynamics_is_used else None
|
|
for i in range(start_atoms, start_atoms + np.sum(num_species), 1):
|
|
line = data[i].split()
|
|
coords.append([float(j) * coords_scale for j in line[:3]])
|
|
if sdynamics_is_used:
|
|
for i in range(start_atoms, start_atoms + np.sum(num_species), 1):
|
|
line = data[i].split()
|
|
sdynamics_data.append([j for j in line[3:6]])
|
|
|
|
struct = Structure(lattice, species, coords, coords_are_cartesian)
|
|
|
|
if sdynamics_is_used:
|
|
return Poscar(struct, comment, sdynamics_data)
|
|
else:
|
|
return Poscar(struct, comment)
|
|
|
|
def to_file(self, filepath: str | Path):
|
|
if isinstance(filepath, str):
|
|
filepath = Path(filepath)
|
|
|
|
file = open(filepath, 'w')
|
|
file.write(f'{self.comment}\n')
|
|
file.write('1\n')
|
|
for vector in self.structure.lattice:
|
|
file.write(f' {vector[0]} {vector[1]} {vector[2]}\n')
|
|
|
|
species = np.array(self.structure.species)
|
|
sorted_order = np.argsort(species, kind='stable')
|
|
unique, counts = np.unique(species, return_counts=True)
|
|
line = ' '
|
|
for u in unique:
|
|
line += u + ' '
|
|
file.write(line + '\n')
|
|
line = ' '
|
|
for c in counts:
|
|
line += str(c) + ' '
|
|
file.write(line + '\n')
|
|
|
|
if self.sdynamics_data is not None:
|
|
file.write('Selective dynamics\n')
|
|
|
|
if self.structure.coords_are_cartesian:
|
|
file.write('Cartesian\n')
|
|
else:
|
|
file.write('Direct\n')
|
|
|
|
if self.sdynamics_data is None:
|
|
for i in sorted_order:
|
|
atom = self.structure.coords[i]
|
|
file.write(f' {atom[0]} {atom[1]} {atom[2]}\n')
|
|
else:
|
|
for i in sorted_order:
|
|
atom = self.structure.coords[i]
|
|
sd_atom = self.sdynamics_data[i]
|
|
file.write(f' {atom[0]} {atom[1]} {atom[2]} {sd_atom[0]} {sd_atom[1]} {sd_atom[2]}\n')
|
|
|
|
file.close()
|
|
|
|
def convert(self, format):
|
|
if format == 'jdftx':
|
|
self.mod_coords_to_cartesian()
|
|
return jdftx.Ionpos(self.structure.species, self.structure.coords * Angstrom2Bohr), \
|
|
jdftx.Lattice(np.transpose(self.structure.lattice) * Angstrom2Bohr)
|
|
else:
|
|
raise ValueError('Only format = jdftx is supported')
|
|
|
|
def mod_add_atoms(self, coords, species, sdynamics_data=None):
|
|
self.structure.mod_add_atoms(coords, species)
|
|
if sdynamics_data is not None:
|
|
if any(isinstance(el, list) for el in sdynamics_data):
|
|
for sd_atom in sdynamics_data:
|
|
self.sdynamics_data.append(sd_atom)
|
|
else:
|
|
self.sdynamics_data.append(sdynamics_data)
|
|
|
|
def mod_change_atoms(self, ids: Union[int, Iterable],
|
|
new_coords: Union[Iterable[float], Iterable[Iterable[float]]] = None,
|
|
new_species: Union[str, List[str]] = None,
|
|
new_sdynamics_data: Union[Iterable[str], Iterable[Iterable[str]]] = None):
|
|
self.structure.mod_change_atoms(ids, new_coords, new_species)
|
|
if new_sdynamics_data is not None:
|
|
if self.sdynamics_data is None:
|
|
self.sdynamics_data = [['T', 'T', 'T'] for _ in range(self.structure.natoms)]
|
|
if isinstance(ids, Iterable):
|
|
for i, new_sdata in zip(ids, new_sdynamics_data):
|
|
self.sdynamics_data[i] = new_sdata
|
|
else:
|
|
self.sdynamics_data[ids] = new_sdynamics_data
|
|
|
|
def mod_coords_to_box(self):
|
|
assert self.structure.coords_are_cartesian is False, 'This operation allowed only for NON-cartesian coords'
|
|
self.structure.coords %= 1
|
|
|
|
def mod_coords_to_direct(self):
|
|
self.structure.mod_coords_to_direct()
|
|
|
|
def mod_coords_to_cartesian(self):
|
|
self.structure.mod_coords_to_cartesian()
|
|
|
|
|
|
class Outcar(EBS, IonicDynamics):
|
|
"""Class that reads VASP OUTCAR files"""
|
|
|
|
def __init__(self,
|
|
weights: NDArray[Shape['Nkpts'], Number],
|
|
efermi_hist: NDArray[Shape['Nisteps'], Number],
|
|
eigenvalues_hist: NDArray[Shape['Nisteps, Nspin, Nkpts, Nbands'], Number],
|
|
occupations_hist: NDArray[Shape['Nisteps, Nspin, Nkpts, Nbands'], Number],
|
|
energy_hist: NDArray[Shape['Nallsteps'], Number],
|
|
energy_ionic_hist: NDArray[Shape['Nisteps'], Number],
|
|
forces_hist: NDArray[Shape['Nispeps, Natoms, 3'], Number]):
|
|
EBS.__init__(self, eigenvalues_hist[-1], weights, efermi_hist[-1], occupations_hist[-1])
|
|
IonicDynamics.__init__(self, forces_hist, None, None, None)
|
|
|
|
self.efermi_hist = efermi_hist
|
|
self.energy_hist = energy_hist
|
|
self.energy_ionic_hist = energy_ionic_hist
|
|
self.eigenvalues_hist = eigenvalues_hist
|
|
self.occupations_hist = occupations_hist
|
|
|
|
def __add__(self, other):
|
|
"""
|
|
Concatenates Outcar files (all histories). It is useful for ionic optimization.
|
|
If k-point meshes from two Outcars are different, weights, eigenvalues and occupations will be taken
|
|
from the 2nd (other) Outcar instance
|
|
Args:
|
|
other (Outcar class): Outcar that should be added to the current Outcar
|
|
|
|
Returns (Outcar class):
|
|
New Outcar with concatenated histories
|
|
"""
|
|
assert isinstance(other, Outcar), 'Other object must belong to Outcar class'
|
|
assert self.natoms == other.natoms, 'Number of atoms of two files must be equal'
|
|
if not np.array_equal(self.weights, other.weights):
|
|
warnings.warn('Two Outcar instances have been calculated with different k-point folding. '
|
|
'Weights, eigenvalues and occupations will be taken from the 2nd (other) instance. '
|
|
'Hope you know, what you are doing')
|
|
return Outcar(other.weights,
|
|
np.concatenate((self.efermi_hist, other.efermi_hist)),
|
|
other.eigenvalues_hist,
|
|
other.occupations_hist,
|
|
np.concatenate((self.energy_hist, other.energy_hist)),
|
|
np.concatenate((self.energy_ionic_hist, other.energy_ionic_hist)),
|
|
np.concatenate((self.forces_hist, other.forces_hist)))
|
|
|
|
return Outcar(other.weights,
|
|
np.concatenate((self.efermi_hist, other.efermi_hist)),
|
|
np.concatenate((self.eigenvalues_hist, other.eigenvalues_hist)),
|
|
np.concatenate((self.occupations_hist, other.occupations_hist)),
|
|
np.concatenate((self.energy_hist, other.energy_hist)),
|
|
np.concatenate((self.energy_ionic_hist, other.energy_ionic_hist)),
|
|
np.concatenate((self.forces_hist, other.forces_hist)))
|
|
|
|
@property
|
|
def natoms(self):
|
|
return self.forces.shape[0]
|
|
|
|
@property
|
|
def nisteps(self):
|
|
return self.energy_ionic_hist.shape[0]
|
|
|
|
@property
|
|
def forces(self):
|
|
return self.forces_hist[-1]
|
|
|
|
@property
|
|
def energy(self):
|
|
return self.energy_ionic_hist[-1]
|
|
|
|
@staticmethod
|
|
def from_file(filepath: str | Path):
|
|
if isinstance(filepath, str):
|
|
filepath = Path(filepath)
|
|
|
|
file = open(filepath, 'r')
|
|
data = file.readlines()
|
|
file.close()
|
|
|
|
patterns = {'nkpts': r'k-points\s+NKPTS\s+=\s+(\d+)',
|
|
'nbands': r'number of bands\s+NBANDS=\s+(\d+)',
|
|
'natoms': r'NIONS\s+=\s+(\d+)',
|
|
'weights': 'Following reciprocal coordinates:',
|
|
'efermi': r'E-fermi\s:\s+([-.\d]+)',
|
|
'energy': r'free energy\s+TOTEN\s+=\s+(.\d+\.\d+)\s+eV',
|
|
'energy_ionic': r'free energy\s+TOTEN\s+=\s+(.\d+\.\d+)\s+eV',
|
|
'kpoints': r'k-point\s+(\d+)\s:\s+[-.\d]+\s+[-.\d]+\s+[-.\d]+\n',
|
|
'forces': r'\s+POSITION\s+TOTAL-FORCE',
|
|
'spin': r'spin component \d+\n'}
|
|
matches = regrep(str(filepath), patterns)
|
|
|
|
nbands = int(matches['nbands'][0][0][0])
|
|
nkpts = int(matches['nkpts'][0][0][0])
|
|
natoms = int(matches['natoms'][0][0][0])
|
|
energy_hist = np.array([float(i[0][0]) for i in matches['energy']])
|
|
energy_ionic_hist = np.array([float(i[0][0]) for i in matches['energy_ionic']])
|
|
|
|
if matches['spin']:
|
|
nspin = 2
|
|
else:
|
|
nspin = 1
|
|
|
|
if nkpts == 1:
|
|
weights = np.array([float(data[matches['weights'][0][1] + 2].split()[3])])
|
|
else:
|
|
weights = np.zeros(nkpts)
|
|
for i in range(nkpts):
|
|
weights[i] = float(data[matches['weights'][0][1] + 2 + i].split()[3])
|
|
weights /= np.sum(weights)
|
|
|
|
arr = matches['efermi']
|
|
efermi_hist = np.zeros(len(arr))
|
|
for i in range(len(arr)):
|
|
efermi_hist[i] = float(arr[i][0][0])
|
|
|
|
nisteps = len(energy_ionic_hist)
|
|
eigenvalues_hist = np.zeros((nisteps, nspin, nkpts, nbands))
|
|
occupations_hist = np.zeros((nisteps, nspin, nkpts, nbands))
|
|
|
|
each_kpoint_list = np.array([[int(j[0][0]), int(j[1])] for j in matches['kpoints']])
|
|
for step in range(nisteps):
|
|
for spin in range(nspin):
|
|
for kpoint in range(nkpts):
|
|
arr = data[each_kpoint_list[nkpts * nspin * step + nkpts * spin + kpoint, 1] + 2:
|
|
each_kpoint_list[nkpts * nspin * step + nkpts * spin + kpoint, 1] + 2 + nbands]
|
|
eigenvalues_hist[step, spin, kpoint] = [float(i.split()[1]) for i in arr]
|
|
occupations_hist[step, spin, kpoint] = [float(i.split()[2]) for i in arr]
|
|
|
|
arr = matches['forces']
|
|
forces_hist = np.zeros((nisteps, natoms, 3))
|
|
for step in range(nisteps):
|
|
for atom in range(natoms):
|
|
line = data[arr[step][1] + atom + 2:arr[step][1] + atom + 3]
|
|
line = line[0].split()
|
|
forces_hist[step, atom] = [float(line[3]), float(line[4]), float(line[5])]
|
|
|
|
return Outcar(weights, efermi_hist, eigenvalues_hist, occupations_hist,
|
|
energy_hist, energy_ionic_hist, forces_hist)
|
|
|
|
|
|
class Wavecar:
|
|
"""Class that reads VASP WAVECAR files"""
|
|
# TODO: add useful functions for Wavecar class: plot charge density, plot real and imag parts etc.
|
|
|
|
def __init__(self, kb_array, wavefunctions, ngrid_factor):
|
|
self.kb_array = kb_array
|
|
self.wavefunctions = wavefunctions
|
|
self.ngrid_factor = ngrid_factor
|
|
|
|
@staticmethod
|
|
def from_file(filepath, kb_array, ngrid_factor=1.5):
|
|
from echem.core.vaspwfc_p3 import vaspwfc
|
|
wfc = vaspwfc(filepath)
|
|
wavefunctions = []
|
|
for kb in kb_array:
|
|
kpoint = kb[0]
|
|
band = kb[1]
|
|
wf = wfc.wfc_r(ikpt=kpoint, iband=band, ngrid=wfc._ngrid * ngrid_factor)
|
|
wavefunctions.append(wf)
|
|
return Wavecar(kb_array, wavefunctions, ngrid_factor)
|
|
|
|
|
|
class Procar:
|
|
def __init__(self, proj_koeffs, orbital_names):
|
|
self.proj_koeffs = proj_koeffs
|
|
self.eigenvalues = None
|
|
self.weights = None
|
|
self.nspin = None
|
|
self.nkpts = None
|
|
self.nbands = None
|
|
self.efermi = None
|
|
self.natoms = None
|
|
self.norbs = proj_koeffs.shape[4]
|
|
self.orbital_names = orbital_names
|
|
|
|
@staticmethod
|
|
def from_file(filepath):
|
|
procar = Procar_pmg(filepath)
|
|
spin_keys = list(procar.data.keys())
|
|
proj_koeffs = np.zeros((len(spin_keys),) + procar.data[spin_keys[0]].shape)
|
|
|
|
for i, spin_key in enumerate(spin_keys):
|
|
proj_koeffs[i] = procar.data[spin_key]
|
|
|
|
return Procar(proj_koeffs, procar.orbitals)
|
|
|
|
def get_PDOS(self, outcar: Outcar, atom_numbers, **kwargs):
|
|
self.eigenvalues = outcar.eigenvalues
|
|
self.weights = outcar.weights
|
|
self.nspin = outcar.nspin
|
|
self.nkpts = outcar.nkpts
|
|
self.nbands = outcar.nbands
|
|
self.efermi = outcar.efermi
|
|
self.natoms = outcar.natoms
|
|
|
|
if 'zero_at_fermi' in kwargs:
|
|
zero_at_fermi = kwargs['zero_at_fermi']
|
|
else:
|
|
zero_at_fermi = False
|
|
|
|
if 'dE' in kwargs:
|
|
dE = kwargs['dE']
|
|
else:
|
|
dE = 0.01
|
|
|
|
if 'smearing' in kwargs:
|
|
smearing = kwargs['smearing']
|
|
else:
|
|
smearing = 'Gaussian'
|
|
|
|
if smearing == 'Gaussian':
|
|
if 'sigma' in kwargs:
|
|
sigma = kwargs['sigma']
|
|
else:
|
|
sigma = 0.02
|
|
if 'emin' in kwargs:
|
|
E_min = kwargs['emin']
|
|
else:
|
|
E_min = np.min(self.eigenvalues)
|
|
if 'emax' in kwargs:
|
|
E_max = kwargs['emax']
|
|
else:
|
|
E_max = np.max(self.eigenvalues)
|
|
else:
|
|
raise ValueError(f'Only Gaussian smearing is supported but you used {smearing} instead')
|
|
|
|
E_arr = np.arange(E_min, E_max, dE)
|
|
ngrid = E_arr.shape[0]
|
|
|
|
proj_coeffs_weighted = self.proj_koeffs[:, :, :, atom_numbers, :]
|
|
|
|
for spin in range(self.nspin):
|
|
for i, weight_kpt in enumerate(self.weights):
|
|
proj_coeffs_weighted[spin, i] *= weight_kpt
|
|
|
|
W_arr = np.moveaxis(proj_coeffs_weighted, [2, 3, 4], [4, 2, 3])
|
|
G_arr = EBS.gaussian_smearing(E_arr, self.eigenvalues, sigma)
|
|
|
|
PDOS_arr = np.zeros((self.nspin, len(atom_numbers), self.norbs, ngrid))
|
|
for spin in range(self.nspin):
|
|
for atom in range(len(atom_numbers)):
|
|
PDOS_arr[spin, atom] = np.sum(G_arr[spin, :, None, :, :] * W_arr[spin, :, atom, :, :, None],
|
|
axis=(0, 2))
|
|
|
|
if self.nspin == 1:
|
|
PDOS_arr *= 2
|
|
|
|
if zero_at_fermi:
|
|
return E_arr - self.efermi, PDOS_arr
|
|
else:
|
|
return E_arr, PDOS_arr
|
|
|
|
|
|
class Chgcar:
|
|
"""
|
|
Class for reading CHG and CHGCAR files from vasp
|
|
For now, we ignore augmentation occupancies data
|
|
"""
|
|
def __init__(self, structure, charge_density, spin_density=None):
|
|
self.structure = structure
|
|
self.charge_density = charge_density
|
|
self.spin_density = spin_density
|
|
|
|
@staticmethod
|
|
def from_file(filepath):
|
|
poscar = Poscar.from_file(filepath)
|
|
structure = poscar.structure
|
|
|
|
volumetric_data = []
|
|
read_data = False
|
|
|
|
with open(filepath, 'r') as file:
|
|
for i in range(8 + structure.natoms):
|
|
file.readline()
|
|
|
|
for line in file:
|
|
line_data = line.strip().split()
|
|
if read_data:
|
|
for value in line_data:
|
|
if i < length - 1:
|
|
data[indexes_1[i], indexes_2[i], indexes_3[i]] = float(value)
|
|
i += 1
|
|
else:
|
|
data[indexes_1[i], indexes_2[i], indexes_3[i]] = float(value)
|
|
read_data = False
|
|
volumetric_data.append(data)
|
|
else:
|
|
if len(line_data) == 3:
|
|
try:
|
|
shape = np.array(list(map(int, line_data)))
|
|
except:
|
|
pass
|
|
else:
|
|
read_data = True
|
|
nx, ny, nz = shape
|
|
data = np.zeros(shape)
|
|
length = np.prod(shape)
|
|
i = 0
|
|
indexes = np.arange(0, length)
|
|
indexes_1 = indexes % nx
|
|
indexes_2 = (indexes // nx) % ny
|
|
indexes_3 = indexes // (nx * ny)
|
|
|
|
if len(volumetric_data) == 1:
|
|
return Chgcar(structure, volumetric_data[0])
|
|
elif len(volumetric_data) == 2:
|
|
return Chgcar(structure, volumetric_data[0], volumetric_data[1])
|
|
else:
|
|
raise ValueError(f'The file contains more than 2 volumetric data, len = {len(volumetric_data)}')
|
|
|
|
def convert_to_cube(self, volumetric_data='charge_density'):
|
|
comment = ' Cube file was created using Electrochemistry package\n'
|
|
if volumetric_data == 'charge_density':
|
|
return Cube(data=self.charge_density,
|
|
structure=self.structure,
|
|
comment=comment+' Charge Density\n',
|
|
origin=np.zeros(3))
|
|
elif volumetric_data == 'spin_density':
|
|
return Cube(data=self.spin_density,
|
|
structure=self.structure,
|
|
comment=comment + ' Spin Density\n',
|
|
origin=np.zeros(3))
|
|
elif volumetric_data == 'spin_major':
|
|
return Cube(data=(self.charge_density + self.spin_density)/2,
|
|
structure=self.structure,
|
|
comment=comment+' Major Spin\n',
|
|
origin=np.zeros(3))
|
|
elif volumetric_data == 'spin_minor':
|
|
return Cube(data=(self.charge_density - self.spin_density)/2,
|
|
structure=self.structure,
|
|
comment=comment+' Minor Spin\n',
|
|
origin=np.zeros(3))
|
|
|
|
def to_file(self, filepath):
|
|
#TODO write to_file func
|
|
pass
|
|
|
|
|
|
class Xdatcar:
|
|
"""Class that reads VASP XDATCAR files"""
|
|
|
|
def __init__(self,
|
|
structure,
|
|
comment: str = None,
|
|
trajectory=None):
|
|
"""
|
|
Create an Xdatcar instance
|
|
Args:
|
|
structure (Structure class): a base class that contains lattice, coords and species information
|
|
comment (str): a VASP comment
|
|
trajectory (3D np.array): contains coordinates of all atoms along with trajectory. It has the shape
|
|
n_steps x n_atoms x 3
|
|
"""
|
|
self.structure = structure
|
|
self.comment = comment
|
|
self.trajectory = trajectory
|
|
|
|
def __add__(self, other):
|
|
"""
|
|
Concatenates Xdatcar files (theirs trajectory)
|
|
Args:
|
|
other (Xdatcar class): Xdatcar that should be added to the current Xdatcar
|
|
|
|
Returns (Xdatcar class):
|
|
New Xdatcar with concatenated trajectory
|
|
"""
|
|
assert isinstance(other, Xdatcar), 'Other object must belong to Xdatcar class'
|
|
assert np.array_equal(self.structure.lattice, other.structure.lattice), 'Lattices of two files must be equal'
|
|
assert self.structure.species == other.structure.species, 'Species in two files must be identical'
|
|
assert self.structure.coords_are_cartesian == other.structure.coords_are_cartesian, \
|
|
'Coords must be in the same coordinate system'
|
|
trajectory = np.vstack((self.trajectory, other.trajectory))
|
|
|
|
return Xdatcar(self.structure, self.comment + ' + ' + other.comment, trajectory)
|
|
|
|
def add(self, other):
|
|
"""
|
|
Concatenates Xdatcar files (theirs trajectory)
|
|
Args:
|
|
other (Xdatcar class): Xdatcar that should be added to the current Xdatcar
|
|
|
|
Returns (Xdatcar class):
|
|
New Xdatcar with concatenated trajectory
|
|
"""
|
|
return self.__add__(other)
|
|
|
|
def add_(self, other):
|
|
"""
|
|
Concatenates Xdatcar files (theirs trajectory). It's inplace operation, current Xdatcar will be modified
|
|
Args:
|
|
other (Xdatcar class): Xdatcar that should be added to the current Xdatcar
|
|
"""
|
|
assert isinstance(other, Xdatcar), 'Other object must belong to Xdatcar class'
|
|
assert np.array_equal(self.structure.lattice, other.structure.lattice), 'Lattices of two files mist be equal'
|
|
assert self.structure.species == other.structure.species, 'Species in two files must be identical'
|
|
assert self.structure.coords_are_cartesian == other.structure.coords_are_cartesian, \
|
|
'Coords must be in the same coordinate system'
|
|
self.trajectory = np.vstack((self.trajectory, other.trajectory))
|
|
|
|
@property
|
|
def nsteps(self):
|
|
return len(self.trajectory)
|
|
|
|
@staticmethod
|
|
def from_file(filepath):
|
|
"""
|
|
Static method to read a XDATCAR file
|
|
Args:
|
|
filepath: path to the XDATCAR file
|
|
|
|
Returns:
|
|
Xdatcar class object
|
|
"""
|
|
file = open(filepath, 'r')
|
|
data = file.readlines()
|
|
file.close()
|
|
|
|
comment = data[0].strip()
|
|
scale = float(data[1])
|
|
lattice = np.array([[float(i) for i in line.split()] for line in data[2:5]])
|
|
if scale < 0:
|
|
# In VASP, a negative scale factor is treated as a volume.
|
|
# We need to translate this to a proper lattice vector scaling.
|
|
vol = abs(np.linalg.det(lattice))
|
|
lattice *= (-scale / vol) ** (1 / 3)
|
|
else:
|
|
lattice *= scale
|
|
|
|
name_species = data[5].split()
|
|
num_species = [int(i) for i in data[6].split()]
|
|
species = []
|
|
for name, num in zip(name_species, num_species):
|
|
species += [name] * num
|
|
|
|
n_atoms = np.sum(num_species)
|
|
n_steps = int((len(data) - 7) / (n_atoms + 1))
|
|
trajectory = np.zeros((n_steps, n_atoms, 3))
|
|
|
|
for i in range(n_steps):
|
|
atom_start = 8 + i * (n_atoms + 1)
|
|
atom_stop = 7 + (i + 1) * (n_atoms + 1)
|
|
data_step = [line.split() for line in data[atom_start:atom_stop]]
|
|
for j in range(n_atoms):
|
|
trajectory[i, j] = [float(k) for k in data_step[j]]
|
|
|
|
struct = Structure(lattice, species, trajectory[0], coords_are_cartesian=False)
|
|
|
|
return Xdatcar(struct, comment, trajectory)
|
|
|
|
def to_file(self, filepath):
|
|
file = open(filepath, 'w')
|
|
file.write(f'{self.comment}\n')
|
|
file.write('1\n')
|
|
for vector in self.structure.lattice:
|
|
file.write(f' {vector[0]} {vector[1]} {vector[2]}\n')
|
|
|
|
species = np.array(self.structure.species)
|
|
sorted_order = np.argsort(species, kind='stable')
|
|
sorted_trajectory = self.trajectory[:, sorted_order, :]
|
|
unique, counts = np.unique(species, return_counts=True)
|
|
line = ' '
|
|
for u in unique:
|
|
line += u + ' '
|
|
file.write(line + '\n')
|
|
line = ' '
|
|
for c in counts:
|
|
line += str(c) + ' '
|
|
file.write(line + '\n')
|
|
|
|
for i in range(self.nsteps):
|
|
file.write(f'Direct configuration= {i + 1}\n')
|
|
for j in range(self.structure.natoms):
|
|
file.write(f' {sorted_trajectory[i, j, 0]} '
|
|
f'{sorted_trajectory[i, j, 1]} '
|
|
f'{sorted_trajectory[i, j, 2]}\n')
|
|
|
|
file.close()
|
|
|
|
def mod_coords_to_cartesian(self):
|
|
if self.structure.coords_are_cartesian is True:
|
|
return 'Coords are already cartesian'
|
|
else:
|
|
self.trajectory = np.matmul(self.trajectory, self.structure.lattice)
|
|
self.structure.mod_coords_to_cartesian()
|
|
|
|
def mod_coords_to_box(self):
|
|
assert self.structure.coords_are_cartesian is False, 'This operation allowed only for NON-cartesian coords'
|
|
self.trajectory %= 1
|
|
self.structure.coords %= 1
|