init
This commit is contained in:
701
electrochemistry/echem/toolchains/jdftx.py
Normal file
701
electrochemistry/echem/toolchains/jdftx.py
Normal file
@@ -0,0 +1,701 @@
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing_extensions import Required, NotRequired, TypedDict
|
||||
from echem.io_data.jdftx import VolumetricData, Output, Lattice, Ionpos, Eigenvals, Fillings, kPts, DOS
|
||||
from echem.io_data.ddec import Output_DDEC
|
||||
from echem.io_data.bader import ACF
|
||||
from echem.core.constants import Hartree2eV, eV2Hartree, Bohr2Angstrom, Angstrom2Bohr, \
|
||||
Bader_radii_Bohr, IDSCRF_radii_Angstrom
|
||||
from echem.core.electronic_structure import EBS
|
||||
from monty.re import regrep
|
||||
from subprocess import Popen, PIPE
|
||||
from timeit import default_timer as timer
|
||||
from datetime import timedelta
|
||||
import matplotlib.pyplot as plt
|
||||
import shutil
|
||||
import re
|
||||
import numpy as np
|
||||
from nptyping import NDArray, Shape, Number
|
||||
from typing import Literal
|
||||
from tqdm.autonotebook import tqdm
|
||||
from termcolor import colored
|
||||
from threading import Lock
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
class System(TypedDict):
|
||||
substrate: str
|
||||
adsorbate: str
|
||||
idx: int
|
||||
output: Output | None
|
||||
nac_ddec: Output_DDEC | None
|
||||
output_phonons: Output | None
|
||||
dos: EBS | None
|
||||
nac_bader: ACF | None
|
||||
excluded_volumes: dict[Literal['cavity', 'molecule', 'free'], float] | None
|
||||
|
||||
|
||||
class DDEC_params(TypedDict):
|
||||
path_atomic_densities: Required[str]
|
||||
path_ddec_executable: NotRequired[str]
|
||||
input_filename: NotRequired[str]
|
||||
periodicity: NotRequired[tuple[bool]]
|
||||
charge_type: NotRequired[str]
|
||||
compute_BOs: NotRequired[bool]
|
||||
number_of_core_electrons: NotRequired[list[list[int]]]
|
||||
|
||||
|
||||
class InfoExtractor:
|
||||
def __init__(self,
|
||||
ddec_params: DDEC_params = None,
|
||||
path_bader_executable: Path | str = None,
|
||||
path_arvo_executable: Path | str = None,
|
||||
systems: list[System] = None,
|
||||
output_name: str = 'output.out',
|
||||
jdftx_prefix: str = 'jdft',
|
||||
do_ddec: bool = False,
|
||||
do_bader: bool = False):
|
||||
|
||||
if ddec_params is not None:
|
||||
if do_ddec and 'path_ddec_executable' not in ddec_params:
|
||||
raise ValueError('"path_ddec_executable" must be specified in ddec_params if do_ddec=True')
|
||||
elif do_ddec:
|
||||
raise ValueError('"ddec_params" mist be specified if do_ddec=True')
|
||||
|
||||
if systems is None:
|
||||
self.systems = []
|
||||
|
||||
if isinstance(path_bader_executable, str):
|
||||
path_bader_executable = Path(path_bader_executable)
|
||||
|
||||
if isinstance(path_arvo_executable, str):
|
||||
path_arvo_executable = Path(path_arvo_executable)
|
||||
|
||||
self.output_name = output_name
|
||||
self.jdftx_prefix = jdftx_prefix
|
||||
self.do_ddec = do_ddec
|
||||
self.ddec_params = ddec_params
|
||||
self.do_bader = do_bader
|
||||
self.path_bader_executable = path_bader_executable
|
||||
self.path_arvo_executable = path_arvo_executable
|
||||
|
||||
self.lock = Lock()
|
||||
|
||||
def create_job_control(self,
|
||||
filepath: str | Path,
|
||||
charge: float,
|
||||
ddec_params: DDEC_params):
|
||||
if isinstance(filepath, str):
|
||||
filepath = Path(filepath)
|
||||
|
||||
if 'path_atomic_densities' in ddec_params:
|
||||
path_atomic_densities = ddec_params['path_atomic_densities']
|
||||
else:
|
||||
raise ValueError('"path_atomic_densities" must be specified in ddec_params dict')
|
||||
|
||||
if 'input_filename' in ddec_params:
|
||||
input_filename = ddec_params['input_filename']
|
||||
else:
|
||||
input_filename = None
|
||||
|
||||
if 'periodicity' in ddec_params:
|
||||
periodicity = ddec_params['periodicity']
|
||||
else:
|
||||
periodicity = (True, True, True)
|
||||
|
||||
if 'charge_type' in ddec_params:
|
||||
charge_type = ddec_params['charge_type']
|
||||
else:
|
||||
charge_type = 'DDEC6'
|
||||
|
||||
if 'compute_BOs' in ddec_params:
|
||||
compute_BOs = ddec_params['compute_BOs']
|
||||
else:
|
||||
compute_BOs = True
|
||||
|
||||
if 'number_of_core_electrons' in ddec_params:
|
||||
number_of_core_electrons = ddec_params['number_of_core_electrons']
|
||||
else:
|
||||
number_of_core_electrons = None
|
||||
|
||||
job_control = open(filepath, 'w')
|
||||
|
||||
job_control.write('<net charge>\n')
|
||||
job_control.write(f'{charge}\n')
|
||||
job_control.write('</net charge>\n\n')
|
||||
|
||||
job_control.write('<atomic densities directory complete path>\n')
|
||||
job_control.write(path_atomic_densities + '\n')
|
||||
job_control.write('</atomic densities directory complete path>\n\n')
|
||||
|
||||
if input_filename is not None:
|
||||
job_control.write('<input filename>\n')
|
||||
job_control.write(input_filename + '\n')
|
||||
job_control.write('</input filename>\n\n')
|
||||
|
||||
job_control.write('<periodicity along A, B, and C vectors>\n')
|
||||
for p in periodicity:
|
||||
if p:
|
||||
job_control.write('.true.\n')
|
||||
else:
|
||||
job_control.write('.false.\n')
|
||||
job_control.write('</periodicity along A, B, and C vectors>\n\n')
|
||||
|
||||
job_control.write('<charge type>\n')
|
||||
job_control.write(charge_type + '\n')
|
||||
job_control.write('</charge type>\n\n')
|
||||
|
||||
job_control.write('<compute BOs>\n')
|
||||
if compute_BOs:
|
||||
job_control.write('.true.\n')
|
||||
else:
|
||||
job_control.write('.false.\n')
|
||||
job_control.write('</compute BOs>\n')
|
||||
|
||||
if number_of_core_electrons is not None:
|
||||
job_control.write('<number of core electrons>\n')
|
||||
for i in number_of_core_electrons:
|
||||
job_control.write(f'{i[0]} {i[1]}\n')
|
||||
job_control.write('</number of core electrons>\n')
|
||||
|
||||
job_control.close()
|
||||
|
||||
def check_out_outvib_sameness(self):
|
||||
for system in self.systems:
|
||||
if system['output'] is not None and system['output_phonons'] is not None:
|
||||
if not system['output'].structure == system['output_phonons'].structure:
|
||||
print(colored('System:', color='red'),
|
||||
colored(' '.join((system['substrate'], system['adsorbate'], str(system['idx']))),
|
||||
color='red', attrs=['bold']),
|
||||
colored('has output and phonon output for different systems'))
|
||||
|
||||
def extract_info_multiple(self,
|
||||
path_root_folder: str | Path,
|
||||
recreate_files: dict[Literal['bader', 'ddec', 'cars', 'cubes', 'volumes'], bool] = None,
|
||||
num_workers: int = 1,
|
||||
parse_folders_names=True) -> None:
|
||||
if isinstance(path_root_folder, str):
|
||||
path_root_folder = Path(path_root_folder)
|
||||
|
||||
subfolders = [f for f in path_root_folder.rglob('*') if f.is_dir()]
|
||||
depth = max([len(f.parents) for f in subfolders])
|
||||
subfolders = [f for f in subfolders if len(f.parents) == depth]
|
||||
|
||||
with tqdm(total=len(subfolders)) as pbar:
|
||||
with ThreadPoolExecutor(num_workers) as executor:
|
||||
for _ in executor.map(self.extract_info, subfolders, [recreate_files] * len(subfolders), \
|
||||
[parse_folders_names] * len(subfolders)):
|
||||
pbar.update()
|
||||
|
||||
def extract_info(self,
|
||||
path_root_folder: str | Path,
|
||||
recreate_files: dict[Literal['bader', 'ddec', 'cars', 'cubes', 'volumes'], bool] = None,
|
||||
parse_folders_names: bool = True) -> None:
|
||||
|
||||
if isinstance(path_root_folder, str):
|
||||
path_root_folder = Path(path_root_folder)
|
||||
|
||||
if recreate_files is None:
|
||||
recreate_files = {'bader': False, 'ddec': False, 'cars': False, 'cubes': False, 'volumes': False}
|
||||
else:
|
||||
if 'bader' not in recreate_files:
|
||||
recreate_files['bader'] = False
|
||||
if 'ddec' not in recreate_files:
|
||||
recreate_files['ddec'] = False
|
||||
if 'cars' not in recreate_files:
|
||||
recreate_files['cars'] = False
|
||||
if 'cubes' not in recreate_files:
|
||||
recreate_files['cubes'] = False
|
||||
if 'volumes' not in recreate_files:
|
||||
recreate_files['volumes'] = False
|
||||
|
||||
files = [file.name for file in path_root_folder.iterdir() if file.is_file()]
|
||||
if parse_folders_names:
|
||||
substrate, adsorbate, idx, *_ = path_root_folder.name.split('_')
|
||||
idx = int(idx)
|
||||
if 'vib' in _:
|
||||
is_vib_folder = True
|
||||
else:
|
||||
is_vib_folder = False
|
||||
if 'bad' in _:
|
||||
return None
|
||||
|
||||
if is_vib_folder:
|
||||
output_phonons = Output.from_file(path_root_folder / self.output_name)
|
||||
if (output_phonons.phonons['zero'] is not None and any(output_phonons.phonons['zero'] > 1e-5)) or \
|
||||
(output_phonons.phonons['imag'] is not None and any(
|
||||
np.abs(output_phonons.phonons['imag']) > 1e-5)):
|
||||
print(colored(str(path_root_folder), color='yellow', attrs=['bold']))
|
||||
|
||||
if output_phonons.phonons['zero'] is not None:
|
||||
string = '['
|
||||
for i in output_phonons.phonons['zero']:
|
||||
if i.imag != 0:
|
||||
string += str(i.real) + '+' + colored(str(i.imag) + 'j', color='yellow',
|
||||
attrs=['bold']) + ', '
|
||||
else:
|
||||
string += str(i.real) + '+' + str(i.imag) + 'j, '
|
||||
string = string[:-2]
|
||||
string += ']'
|
||||
print(f'\t{len(output_phonons.phonons["zero"])} zero modes: {string}')
|
||||
|
||||
if output_phonons.phonons['imag'] is not None:
|
||||
string = '['
|
||||
for i in output_phonons.phonons['imag']:
|
||||
if i.imag != 0:
|
||||
string += str(i.real) + '+' + colored(str(i.imag) + 'j', color='yellow',
|
||||
attrs=['bold']) + ', '
|
||||
else:
|
||||
string += str(i.real) + '+' + str(i.imag) + 'j, '
|
||||
string = string[:-2]
|
||||
string += ']'
|
||||
print(f'\t{len(output_phonons.phonons["imag"])} imag modes: {string}')
|
||||
|
||||
output = None
|
||||
else:
|
||||
output = Output.from_file(path_root_folder / self.output_name)
|
||||
output_phonons = None
|
||||
else:
|
||||
is_vib_folder = False
|
||||
output = Output.from_file(path_root_folder / self.output_name)
|
||||
|
||||
if not is_vib_folder:
|
||||
|
||||
if 'POSCAR' not in files or recreate_files['cars']:
|
||||
print('Create POSCAR for\t\t\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
poscar = output.get_poscar()
|
||||
poscar.to_file(path_root_folder / 'POSCAR')
|
||||
|
||||
if 'CONTCAR' not in files or recreate_files['cars']:
|
||||
print('Create CONTCAR for\t\t\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
contcar = output.get_contcar()
|
||||
contcar.to_file(path_root_folder / 'CONTCAR')
|
||||
|
||||
if 'XDATCAR' not in files or recreate_files['cars']:
|
||||
print('Create XDATCAR for\t\t\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
xdatcar = output.get_xdatcar()
|
||||
xdatcar.to_file(path_root_folder / 'XDATCAR')
|
||||
|
||||
fft_box_size = output.fft_box_size
|
||||
if 'output_volumetric.out' in files:
|
||||
files.remove('output_volumetric.out')
|
||||
patterns = {'fft_box_size': r'Chosen fftbox size, S = \[(\s+\d+\s+\d+\s+\d+\s+)\]'}
|
||||
matches = regrep(str(path_root_folder / 'output_volumetric.out'), patterns)
|
||||
fft_box_size = np.array([int(i) for i in matches['fft_box_size'][0][0][0].split()])
|
||||
|
||||
if 'valence_density.cube' not in files or recreate_files['cubes']:
|
||||
print('Create valence(spin)_density for\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
if f'{self.jdftx_prefix}.n_up' in files and f'{self.jdftx_prefix}.n_dn' in files:
|
||||
n_up = VolumetricData.from_file(path_root_folder / f'{self.jdftx_prefix}.n_up',
|
||||
fft_box_size,
|
||||
output.structure).convert_to_cube()
|
||||
n_dn = VolumetricData.from_file(path_root_folder / f'{self.jdftx_prefix}.n_dn',
|
||||
fft_box_size,
|
||||
output.structure).convert_to_cube()
|
||||
n = n_up + n_dn
|
||||
n.to_file(path_root_folder / 'valence_density.cube')
|
||||
valence_density_exist = True
|
||||
|
||||
if output.magnetization_abs > 1e-2:
|
||||
n = n_up - n_dn
|
||||
n.to_file(path_root_folder / 'spin__density.cube')
|
||||
|
||||
elif f'{self.jdftx_prefix}.n' in files:
|
||||
n = VolumetricData.from_file(path_root_folder / f'{self.jdftx_prefix}.n',
|
||||
fft_box_size,
|
||||
output.structure).convert_to_cube()
|
||||
n.to_file(path_root_folder / 'valence_density.cube')
|
||||
valence_density_exist = True
|
||||
else:
|
||||
print(colored('(!) There is no files for valence(spin)_density.cube creation',
|
||||
color='red', attrs=['bold']))
|
||||
valence_density_exist = False
|
||||
else:
|
||||
valence_density_exist = True
|
||||
|
||||
if ('nbound.cube' not in files or recreate_files['cubes']) and f'{self.jdftx_prefix}.nbound' in files:
|
||||
print('Create nbound.cube for\t\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
nbound = VolumetricData.from_file(path_root_folder / f'{self.jdftx_prefix}.nbound',
|
||||
fft_box_size, output.structure).convert_to_cube()
|
||||
nbound.to_file(path_root_folder / 'nbound.cube')
|
||||
|
||||
for file in files:
|
||||
if file.startswith(f'{self.jdftx_prefix}.fluidN_'):
|
||||
fluid_type = file.removeprefix(self.jdftx_prefix + '.')
|
||||
if f'{fluid_type}.cube' not in files or recreate_files['cubes']:
|
||||
print(f'Create {fluid_type}.cube for\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
fluidN = VolumetricData.from_file(path_root_folder / file,
|
||||
fft_box_size,
|
||||
output.structure).convert_to_cube()
|
||||
fluidN.to_file(path_root_folder / (fluid_type + '.cube'))
|
||||
|
||||
if self.ddec_params is not None and ('job_control.txt' not in files or recreate_files['ddec']):
|
||||
print('Create job_control.txt for\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
charge = - (output.nelec_hist[-1] - output.nelec_pzc)
|
||||
self.create_job_control(filepath=path_root_folder / 'job_control.txt',
|
||||
charge=charge,
|
||||
ddec_params=self.ddec_params)
|
||||
|
||||
if 'ACF.dat' in files and not recreate_files['bader']:
|
||||
nac_bader = ACF.from_file(path_root_folder / 'ACF.dat')
|
||||
nac_bader.nelec_per_isolated_atom = np.array([output.pseudopots[key] for key in
|
||||
output.structure.species])
|
||||
elif self.do_bader and valence_density_exist:
|
||||
print('Run Bader for\t\t\t\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
|
||||
if parse_folders_names:
|
||||
string = str(path_root_folder.name.split('_')[1])
|
||||
print_com = ''
|
||||
if string != 'Pristine':
|
||||
print_com += ' -o atoms'
|
||||
length = len(re.findall(r'[A-Z]', string))
|
||||
ints = [int(i) for i in re.findall(r'[2-9]', re.sub(r'minus\d+.\d+|plus\d+.\d+', '', string))]
|
||||
length += sum(ints) - len(ints)
|
||||
while length > 0:
|
||||
print_com += f' -i {output.structure.natoms + 1 - length}'
|
||||
length -= 1
|
||||
|
||||
spin_com = ''
|
||||
if f'{self.jdftx_prefix}.n_up' in files and \
|
||||
f'{self.jdftx_prefix}.n_dn' in files and \
|
||||
output.magnetization_abs > 1e-2:
|
||||
spin_com = ' -s ' + str(path_root_folder / 'spin__density.cube')
|
||||
else:
|
||||
spin_com = ''
|
||||
print_com = ''
|
||||
com = str(self.path_bader_executable) + ' -t cube' + \
|
||||
print_com + spin_com + ' ' + str(path_root_folder / 'valence_density.cube')
|
||||
p = Popen(com, cwd=path_root_folder)
|
||||
p.wait()
|
||||
|
||||
nac_bader = ACF.from_file(path_root_folder / 'ACF.dat')
|
||||
nac_bader.nelec_per_isolated_atom = np.array([output.pseudopots[key] for key in
|
||||
output.structure.species])
|
||||
else:
|
||||
nac_bader = None
|
||||
|
||||
if not recreate_files['ddec'] and 'valence_cube_DDEC_analysis.output' in files:
|
||||
nac_ddec = Output_DDEC.from_file(path_root_folder / 'valence_cube_DDEC_analysis.output')
|
||||
elif self.ddec_params is not None and self.do_ddec and valence_density_exist:
|
||||
print('Run DDEC for\t\t\t\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
start = timer()
|
||||
|
||||
p = Popen(str(self.ddec_params['path_ddec_executable']), stdin=PIPE, bufsize=0)
|
||||
p.communicate(str(path_root_folder).encode('ascii'))
|
||||
end = timer()
|
||||
print(f'DDEC Finished! Elapsed time: {str(timedelta(seconds=end-start)).split(".")[0]}',
|
||||
colored(str(path_root_folder), attrs=['bold']))
|
||||
nac_ddec = Output_DDEC.from_file(path_root_folder / 'valence_cube_DDEC_analysis.output')
|
||||
else:
|
||||
nac_ddec = None
|
||||
|
||||
if f'{self.jdftx_prefix}.eigenvals' in files and f'{self.jdftx_prefix}.kPts' in files:
|
||||
eigs = Eigenvals.from_file(path_root_folder / f'{self.jdftx_prefix}.eigenvals',
|
||||
output)
|
||||
kpts = kPts.from_file(path_root_folder / f'{self.jdftx_prefix}.kPts')
|
||||
if f'{self.jdftx_prefix}.fillings' in files:
|
||||
occs = Fillings.from_file(path_root_folder / f'{self.jdftx_prefix}.fillings',
|
||||
output).occupations
|
||||
else:
|
||||
occs = None
|
||||
dos = DOS(eigenvalues=eigs.eigenvalues * Hartree2eV,
|
||||
weights=kpts.weights,
|
||||
efermi=output.mu * Hartree2eV,
|
||||
occupations=occs)
|
||||
else:
|
||||
dos = None
|
||||
|
||||
if f'output_phonon.out' in files:
|
||||
output_phonons = Output.from_file(path_root_folder / 'output_phonon.out')
|
||||
if (output_phonons.phonons['zero'] is not None and any(output_phonons.phonons['zero'] > 1e-5)) or \
|
||||
(output_phonons.phonons['imag'] is not None and any(
|
||||
np.abs(output_phonons.phonons['imag']) > 1e-5)):
|
||||
print(colored(str(path_root_folder), color='yellow', attrs=['bold']))
|
||||
if output_phonons.phonons['zero'] is not None:
|
||||
string = '['
|
||||
for i in output_phonons.phonons['zero']:
|
||||
if i.imag != 0:
|
||||
string += str(i.real) + '+' + colored(str(i.imag) + 'j', color='yellow',
|
||||
attrs=['bold']) + ', '
|
||||
else:
|
||||
string += str(i.real) + '+' + str(i.imag) + 'j, '
|
||||
string = string[:-2]
|
||||
string += ']'
|
||||
print(f'\t{len(output_phonons.phonons["zero"])} zero modes: {string}')
|
||||
|
||||
if output_phonons.phonons['imag'] is not None:
|
||||
string = '['
|
||||
for i in output_phonons.phonons['imag']:
|
||||
if i.imag != 0:
|
||||
string += str(i.real) + '+' + colored(str(i.imag) + 'j', color='yellow',
|
||||
attrs=['bold']) + ', '
|
||||
else:
|
||||
string += str(i.real) + '+' + str(i.imag) + 'j, '
|
||||
string = string[:-2]
|
||||
string += ']'
|
||||
print(f'\t{len(output_phonons.phonons["imag"])} imag modes: {string}')
|
||||
|
||||
if parse_folders_names and substrate == 'Mol':
|
||||
if 'bader.ats' not in files or recreate_files['volumes']:
|
||||
file = open(path_root_folder / 'bader.ats', 'w')
|
||||
for name, coord in zip(output.structure.species, output.structure.coords):
|
||||
file.write(f' {coord[0]} {coord[1]} {coord[2]} {Bader_radii_Bohr[name] * Bohr2Angstrom}\n')
|
||||
file.close()
|
||||
|
||||
if 'idscrf.ats' not in files or recreate_files['volumes']:
|
||||
file = open(path_root_folder / 'idscrf.ats', 'w')
|
||||
for name, coord in zip(output.structure.species, output.structure.coords):
|
||||
file.write(f' {coord[0]} {coord[1]} {coord[2]} {IDSCRF_radii_Angstrom[name]}\n')
|
||||
file.close()
|
||||
|
||||
if 'arvo.bader.log' not in files or recreate_files['volumes']:
|
||||
print('Run ARVO.bader for\t\t\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
com = str(self.path_arvo_executable) + ' protein=bader.ats log=arvo.bader.log'
|
||||
p = Popen(com, cwd=path_root_folder)
|
||||
p.wait()
|
||||
|
||||
if 'arvo.idscrf.log' not in files or recreate_files['volumes']:
|
||||
print('Run ARVO.idscrf for\t\t\t\t\t', colored(str(path_root_folder), attrs=['bold']))
|
||||
com = str(self.path_arvo_executable) + ' protein=idscrf.ats log=arvo.idscrf.log'
|
||||
p = Popen(com, cwd=path_root_folder)
|
||||
p.wait()
|
||||
|
||||
excluded_volumes = {}
|
||||
file = open(path_root_folder / 'arvo.bader.log')
|
||||
excluded_volumes['molecule'] = float(file.readline().split()[1])
|
||||
file.close()
|
||||
file = open(path_root_folder / 'arvo.idscrf.log')
|
||||
excluded_volumes['cavity'] = float(file.readline().split()[1])
|
||||
file.close()
|
||||
excluded_volumes['free'] = (excluded_volumes['cavity']**(1/3) - excluded_volumes['molecule']**(1/3))**3
|
||||
|
||||
else:
|
||||
excluded_volumes = None
|
||||
|
||||
else:
|
||||
nac_ddec = None
|
||||
nac_bader = None
|
||||
dos = None
|
||||
excluded_volumes = None
|
||||
|
||||
if parse_folders_names:
|
||||
self.lock.acquire()
|
||||
|
||||
system_proccessed = self.get_system(substrate, adsorbate, idx)
|
||||
if len(system_proccessed) == 1:
|
||||
if output_phonons is not None:
|
||||
system_proccessed[0]['output_phonons'] = output_phonons
|
||||
else:
|
||||
system_proccessed[0]['output'] = output
|
||||
system_proccessed[0]['nac_ddec'] = nac_ddec
|
||||
system_proccessed[0]['dos'] = dos
|
||||
system_proccessed[0]['nac_bader'] = nac_bader
|
||||
system_proccessed[0]['excluded_volumes'] = excluded_volumes
|
||||
|
||||
self.lock.release()
|
||||
|
||||
elif len(system_proccessed) == 0:
|
||||
system: System = {'substrate': substrate,
|
||||
'adsorbate': adsorbate,
|
||||
'idx': idx,
|
||||
'output': output,
|
||||
'nac_ddec': nac_ddec,
|
||||
'output_phonons': output_phonons,
|
||||
'dos': dos,
|
||||
'nac_bader': nac_bader,
|
||||
'excluded_volumes': excluded_volumes}
|
||||
|
||||
self.systems.append(system)
|
||||
self.lock.release()
|
||||
|
||||
else:
|
||||
self.lock.release()
|
||||
raise ValueError(f'There should be 0 ot 1 copy of the system in the InfoExtractor.'
|
||||
f'However there are {len(system_proccessed)} systems copies of following system: '
|
||||
f'{substrate=}, {adsorbate=}, {idx=}')
|
||||
|
||||
def get_system(self, substrate: str, adsorbate: str, idx: int = None) -> list[System]:
|
||||
if idx is None:
|
||||
return [system for system in self.systems if
|
||||
system['substrate'] == substrate and system['adsorbate'] == adsorbate]
|
||||
else:
|
||||
return [system for system in self.systems if
|
||||
system['substrate'] == substrate and system['adsorbate'] == adsorbate and system['idx'] == idx]
|
||||
|
||||
def get_F(self, substrate: str, adsorbate: str, idx: int,
|
||||
units: Literal['eV', 'Ha'] = 'eV',
|
||||
T: float | int = None) -> float:
|
||||
|
||||
if T is None:
|
||||
E = self.get_system(substrate, adsorbate, idx)[0]['output'].energy_ionic_hist['F'][-1]
|
||||
if units == 'Ha':
|
||||
return E
|
||||
elif units == 'eV':
|
||||
return E * Hartree2eV
|
||||
else:
|
||||
raise ValueError(f'units should be "Ha" or "eV" however "{units}" was given')
|
||||
elif isinstance(T, float | int):
|
||||
E = self.get_system(substrate, adsorbate, idx)[0]['output'].energy_ionic_hist['F'][-1]
|
||||
E_vib = self.get_Gibbs_vib(substrate, adsorbate, idx, T)
|
||||
if units == 'Ha':
|
||||
return E + E_vib * eV2Hartree
|
||||
elif units == 'eV':
|
||||
return E * Hartree2eV + E_vib
|
||||
else:
|
||||
raise ValueError(f'units should be "Ha" or "eV" however "{units}" was given')
|
||||
else:
|
||||
raise ValueError(f'T should be None, float or int, but {type(T)} was given')
|
||||
|
||||
def get_G(self, substrate: str, adsorbate: str, idx: int,
|
||||
units: Literal['eV', 'Ha'] = 'eV',
|
||||
T: float | int = None) -> float:
|
||||
if T is None:
|
||||
E = self.get_system(substrate, adsorbate, idx)[0]['output'].energy_ionic_hist['G'][-1]
|
||||
if units == 'Ha':
|
||||
return E
|
||||
elif units == 'eV':
|
||||
return E * Hartree2eV
|
||||
else:
|
||||
raise ValueError(f'units should be "Ha" or "eV" however "{units}" was given')
|
||||
elif isinstance(T, float | int):
|
||||
E = self.get_system(substrate, adsorbate, idx)[0]['output'].energy_ionic_hist['G'][-1]
|
||||
E_vib = self.get_Gibbs_vib(substrate, adsorbate, idx, T)
|
||||
if units == 'Ha':
|
||||
return E + E_vib * eV2Hartree
|
||||
elif units == 'eV':
|
||||
return E * Hartree2eV + E_vib
|
||||
else:
|
||||
raise ValueError(f'units should be "Ha" or "eV" however "{units}" was given')
|
||||
else:
|
||||
raise ValueError(f'T should be None, float or int, but {type(T)} was given')
|
||||
|
||||
def get_N(self, substrate: str, adsorbate: str, idx: int) -> float:
|
||||
return self.get_system(substrate, adsorbate, idx)[0]['output'].nelec
|
||||
|
||||
def get_mu(self, substrate: str, adsorbate: str, idx: int) -> float:
|
||||
return self.get_system(substrate, adsorbate, idx)[0]['output'].mu
|
||||
|
||||
def get_Gibbs_vib(self, substrate: str, adsorbate: str, idx: int, T: float) -> float:
|
||||
return self.get_system(substrate, adsorbate, idx)[0]['output_phonons'].thermal_props.get_Gibbs_vib(T)
|
||||
|
||||
def plot_energy(self, substrate: str, adsorbate: str):
|
||||
|
||||
systems = self.get_system(substrate, adsorbate)
|
||||
energy_min = min(system['output'].energy for system in systems)
|
||||
|
||||
i, j = np.divmod(len(systems), 3)
|
||||
i += 1
|
||||
if j == 0:
|
||||
i -= 1
|
||||
|
||||
fig, axs = plt.subplots(int(i), 3, figsize=(25, 5 * i), dpi=180)
|
||||
fig.subplots_adjust(wspace=0.3, hspace=0.2)
|
||||
|
||||
for system, ax_e in zip(systems, axs.flatten()):
|
||||
out = system['output']
|
||||
|
||||
delta_ionic_energy = (out.energy_ionic_hist['F'] - out.energy_ionic_hist['F'][-1]) * Hartree2eV
|
||||
if (delta_ionic_energy < 0).any():
|
||||
energy_modulus_F = True
|
||||
delta_ionic_energy = np.abs(delta_ionic_energy)
|
||||
else:
|
||||
energy_modulus_F = False
|
||||
|
||||
ax_e.plot(range(out.nisteps), delta_ionic_energy, color='r', label=r'$\Delta F$', ms=3, marker='o')
|
||||
|
||||
if 'G' in out.energy_ionic_hist.keys():
|
||||
delta_ionic_energy = (out.energy_ionic_hist['G'] - out.energy_ionic_hist['G'][-1]) * Hartree2eV
|
||||
if (delta_ionic_energy < 0).any():
|
||||
energy_modulus_G = True
|
||||
delta_ionic_energy = np.abs(delta_ionic_energy)
|
||||
else:
|
||||
energy_modulus_G = False
|
||||
else:
|
||||
energy_modulus_G = None
|
||||
|
||||
ax_e.plot(range(out.nisteps), delta_ionic_energy, color='orange', label=r'$\Delta G$', ms=3, marker='o')
|
||||
|
||||
ax_e.set_yscale('log')
|
||||
ax_e.set_xlabel(r'$Step$', fontsize=12)
|
||||
if energy_modulus_F:
|
||||
ylabel = r'$|\Delta F|, \ $'
|
||||
else:
|
||||
ylabel = r'$\Delta F, \ $'
|
||||
if energy_modulus_G is not None:
|
||||
if energy_modulus_G:
|
||||
ylabel += r'$|\Delta G|, \ $'
|
||||
else:
|
||||
ylabel += r'$\Delta G, \ $'
|
||||
ylabel += r'$eV$'
|
||||
|
||||
ax_e.set_ylabel(ylabel, color='r', fontsize=14)
|
||||
ax_e.legend(loc='upper right', fontsize=14)
|
||||
|
||||
delta_E = (out.energy - energy_min) * Hartree2eV
|
||||
if np.abs(delta_E) < 1e-8:
|
||||
ax_e.text(0.5, 0.9, rf'$\mathbf{{E_f - E_f^{{min}} = {np.round(delta_E, 2)} \ eV}}$', ha='center', va='center', transform=ax_e.transAxes, fontsize=12)
|
||||
ax_e.set_title(rf'$\mathbf{{ {substrate} \ {adsorbate} \ {system["idx"]} }}$', fontsize=13, y=1, pad=-15)
|
||||
else:
|
||||
ax_e.text(0.5, 0.9, rf'$E_f - E_f^{{min}} = {np.round(delta_E, 2)} \ eV$', ha='center', va='center', transform=ax_e.transAxes, fontsize=12)
|
||||
ax_e.set_title(rf'${substrate} \ {adsorbate} \ {system["idx"]}$', fontsize=13, y=1, pad=-15)
|
||||
|
||||
ax_f = ax_e.twinx()
|
||||
ax_f.plot(range(len(out.get_forces())), out.get_forces() * Hartree2eV / (Bohr2Angstrom ** 2), color='g', label=r'$\left< |\vec{F}| \right>$', ms=3, marker='o')
|
||||
ax_f.set_ylabel(r'$Average \ Force, \ eV / \AA^3$', color='g', fontsize=13)
|
||||
ax_f.legend(loc='upper right', bbox_to_anchor=(1, 0.8), fontsize=13)
|
||||
ax_f.set_yscale('log')
|
||||
|
||||
|
||||
def create_z_displacements(folder_source: str | Path,
|
||||
folder_result: str | Path,
|
||||
n_atoms_mol: int,
|
||||
scan_range: NDArray[Shape['Nsteps'], Number] | list[float],
|
||||
create_flat_surface: bool = False,
|
||||
folder_files_to_copy: str | Path = None) -> None:
|
||||
"""
|
||||
Create folder with all necessary files for displacing the selected atoms along z-axis
|
||||
Args:
|
||||
folder_source: path for the folder with .lattice and .ionpos JDFTx files that will be initial files for configurations
|
||||
folder_result: path for the folder where all final files will be saved
|
||||
n_atoms_mol: number of atoms that should be displaced. All atoms must be in the end atom list in .ionpos
|
||||
scan_range: array with displacement (in angstroms) for the selected atoms
|
||||
create_flat_surface: if True all atoms will be projected into graphene surface; if False all atoms except molecules remain at initial positions
|
||||
folder_files_to_copy: path for the folder with input.in and run.sh files to copy into each folder with final configurations
|
||||
"""
|
||||
if isinstance(folder_source, str):
|
||||
folder_source = Path(folder_source)
|
||||
if isinstance(folder_result, str):
|
||||
folder_result = Path(folder_result)
|
||||
if isinstance(folder_files_to_copy, str):
|
||||
folder_files_to_copy = Path(folder_files_to_copy)
|
||||
|
||||
substrate, adsorbate, idx, *_ = folder_source.name.split('_')
|
||||
lattice = Lattice.from_file(folder_source / 'jdft.lattice')
|
||||
|
||||
for d_ang in scan_range:
|
||||
d_ang = np.round(d_ang, 2)
|
||||
d_bohr = d_ang * Angstrom2Bohr
|
||||
|
||||
ionpos = Ionpos.from_file(folder_source / 'jdft.ionpos')
|
||||
|
||||
Path(folder_result / f'{substrate}_{adsorbate}_{idx}/{d_ang}').mkdir(parents=True, exist_ok=True)
|
||||
ionpos.coords[-n_atoms_mol:, 2] += d_bohr
|
||||
|
||||
idx_surf = [i for i, coord in enumerate(ionpos.coords) if np.abs(coord[0]) < 1 or np.abs(coord[1]) < 1]
|
||||
z_carbon = np.mean(ionpos.coords[idx_surf], axis=0)[2]
|
||||
|
||||
if create_flat_surface:
|
||||
ionpos.coords[:-n_atoms_mol, 2] = z_carbon
|
||||
else:
|
||||
ionpos.coords[idx_surf, 2] = z_carbon
|
||||
|
||||
ionpos.move_scale[-n_atoms_mol:] = 0
|
||||
ionpos.move_scale[idx_surf] = 0
|
||||
|
||||
ionpos.to_file(folder_result / f'{substrate}_{adsorbate}_{idx}/{d_ang}/jdft.ionpos')
|
||||
lattice.to_file(folder_result / f'{substrate}_{adsorbate}_{idx}/{d_ang}/jdft.lattice')
|
||||
poscar = ionpos.convert('vasp', lattice)
|
||||
poscar.to_file(folder_result / f'{substrate}_{adsorbate}_{idx}/{d_ang}/POSCAR')
|
||||
|
||||
shutil.copyfile(folder_files_to_copy / 'input.in', folder_result / f'{substrate}_{adsorbate}_{idx}/{d_ang}/input.in')
|
||||
shutil.copyfile(folder_files_to_copy / 'run.sh', folder_result / f'{substrate}_{adsorbate}_{idx}/{d_ang}/run.sh')
|
||||
Reference in New Issue
Block a user