Source code for findtde.defect_analysis

#!/usr/bin/env python3
"""Python module used to characterize defects resulting from TDE calculations."""
import os
import sys
import glob
import pprint
import fortranformat as ff

import math
import numpy as np
import pandas as pd

import warnings
warnings.filterwarnings('ignore', message='.*OVITO.*PyPI')

import ovito._extensions.pyscript

from ovito.io import *
from ovito.data import *
from ovito.modifiers import *
from ovito.pipeline import *


[docs] def find_contcars(base_path, pseudo='*'): """ Function to create an array of post-CGM CONTCAR files for defect analysis. """ contcar_files = np.array(glob.glob(os.path.join(base_path, pseudo, '*', 'cgm', 'CONTCAR'), recursive=True)) return contcar_files
[docs] def find_dumpfiles(base_path, pseudo='*'): """ Function to create an array of post-MD dump files for defect analysis. """ dump_files = np.array(glob.glob(os.path.join(base_path, pseudo, '*', 'dump.final'), recursive=True)) return dump_files
[docs] def pseudo_keys_from_file(pseudo_keyfile='pseudo_keys.csv'): """ Function to convert a CSV file of pseudo keys to a dictionary. """ pk_df = pd.read_csv(pseudo_keyfile, index_col=0) pks_dict = pk_df.to_dict('split') pks_dict = dict(zip(pks_dict['index'], pks_dict['data'])) return pks_dict
[docs] def PBC_distance(x0, x1, dimensions): """ Source: https://stackoverflow.com/a/11109336 Function that takes 3 numpy arrays: an initial position array, a final position array, and the dimensions of the boundary. Returns an array for the distance, taking into account Periodic Boundary Conditions (PBC). """ delta = np.abs(x0 - x1) delta = np.where(delta > 0.5 * dimensions, delta - dimensions, delta) return np.sqrt((delta ** 2).sum(axis=-1))
[docs] def cart2sph(x, y, z): dxy = np.sqrt(x**2 + y**2) r = np.sqrt(dxy**2 + z**2) theta = np.arctan2(y, x) phi = np.arctan2(dxy, z) theta, phi = np.rad2deg([theta, phi]) return r, theta % 360, phi
[docs] def lat2sph(uvw, ai): """ Function converting from lattice directions to spherical coordinates. Given two 2D arrays, one for lattice directions and one for lattice vectors, returns a 2D array of the spherical coordinates corresponding to the lattice directions. """ xyz, rpt = np.zeros(uvw.shape), np.zeros(uvw.shape) for i in range(uvw.shape[0]): xyz[i, :] = uvw[i, :]@ai rpt[i, :] = np.around(cart2sph(xyz[i, 0], xyz[i, 1], xyz[i, 2]), decimals=2) rpt[i, 0] = 1.00 return rpt
########################################################################### ######################## PYMATGEN DEFECTS ANALYSIS ######################## ###########################################################################
[docs] def find_defects_multi_files(base_pos_path, cont_paths): """ Given an initial POSCAR file and any number of post-AIMD, post-CGM CONTCAR files, returns the defect positions by comparing final atomic positions to initial atomic positions. File input is a string for the POSCAR filepath and an array of strings for the CONTCAR filepaths. """ base_struct = Structure.from_file(base_pos_path) dsf = finder.DefectSiteFinder() defect_pos = np.zeros((cont_paths.shape[0], 3)) for q in range(cont_paths.shape[0]): defect_struct = Structure.from_file(cont_paths[q]) fpos = dsf.get_defect_fpos(defect_structure=defect_struct, base_structure=base_struct) fpos -= np.round(fpos) defect_pos[q, :] = fpos return defect_pos
[docs] def fp_sep_dist(base_pos_path, cont_paths, lattice_vecs, atom_type='ga', atom_no=34): """ Given an initial POSCAR file and any number of post-AIMD, post-CGM CONTCAR files, returns the Frenkel pair separation distance after determining defect positions in the structure. File input is a string for the POSCAR filepath and an array of strings for the CONTCAR filepaths. """ # this method assumes the vacancy is at the initial atomic position defect_pos = find_defects_multi_files(base_pos_path, cont_paths)@lattice_vecs v_pos = np.zeros((defect_pos.shape[0], 3)) fp_sep = np.zeros((defect_pos.shape[0], 1)) # open POSCAR file and read lines into a list pos_f = open(base_pos_path, 'r') pos_lines = pos_f.readlines() pos_f.close() # convert species names line from POSCAR file from Fortran to a list pos_spec_line = ff.FortranRecordReader('(2A5)') pos_species = pos_lines[5] pos_spec_list = pos_spec_line.read(pos_species) for i in range(len(pos_spec_list)): pos_spec_list[i] = pos_spec_list[i].strip() # convert ions per species line from POSCAR file from Fortran to a list pos_no_line = ff.FortranRecordReader('(2I6)') ion_nos = pos_lines[6] no_list = pos_no_line.read(ion_nos) total_ions = sum(no_list) # convert position line from POSCAR file from Fortran to a list pos_line = ff.FortranRecordReader('(3F20.16)') for i in range(len(pos_spec_list)): if atom_type.lower() in pos_spec_list[i].lower(): atom_pos_line = 8+atom_no+sum(no_list[:i]) for n in range(defect_pos.shape[0]): v_pos[n, :] = pos_line.read(pos_lines[atom_pos_line].strip('\n')) fp_sep[n] = math.dist(defect_pos[n, :], np.array([0, 0, 0])) return fp_sep
# print('######################## PYMATGEN DEFECTS ANALYSIS ########################') # print(find_defects_multi_files(poscar_path, contcar_paths)@gan_lattice_vecs) # print(fp_sep_dist(poscar_path, contcar_paths, gan_lattice_vecs, atom_type='ga', atom_no=34)) ########################################################################### ############################ OVITO WS ANALYSIS ############################ ########################################################################### """ how this needs to work can probably just use a single function for everything, don't really have much repetition WS analysis: output_displaced = False - returns the vacant site location and the other atom closest to final interstitial - will not know which is the vacant site yet - can get the vacancy and interstitial count info here WS analysis: output_displaced = True - returns interstitial site location and other atom closest to final interstitial - site that matches previous WS analysis can be thrown away, other site from first is v and other site from second is i - need to choose a cutoff such that the additional site is either thrown away or considered as a dumbbell - need for returning proper displacement vectors, so should be done second - also need site type/index/identifier properties from this Displacements modifier - find displacement vectors, should be able to use the site index from second WS to determine final position of interstitial - use this and site type/identifier to determine what type of interstitial and what position final info of desire v & i count (get directly from first WS) v & i type: Ga/N (get directly from second WS?) i site: t/o/db (disp mod and something else idk yet) if v & i type are same (they should be, i think only case where they aren't is if there are multiple defects): separation distance (both WS and calculate distance formula) main issues currently: <---- this was for the previous version of this file, may not be relevant anymore - problem regarding cutoff separating atoms inhabiting the lattice site closest to the interstitial being regarded as so vs. split dumbbell interstitial - could be a problem with defining the array masks to delete rows (should they be the same? or could they be in different orders) - this trickles down into the FP separation distance calculation - need to use the displacement magnitude/vectors to determine whether interstitials are tetrahedral or octahedral - also potential issues with runs like the [120] direction (17L?) where ovito recognizes 2 FPs (1 Ga, 1 N) as 1 FP - need to bring in other info such as direction associated with pseudo (should be easy, use key_dict again) """
[docs] def defect_analysis(defect_file, ref_file, verbose=False): """ Perform WS analyses """ ws_comb_dict = { 'sites': {}, 'atoms': {} } pipeline = import_file(defect_file) # displace atom config data = pipeline.compute() disp_type = data.particles['Particle Type'] disp_pos = data.particles['Position'][...] # Perform Wigner-Seitz analysis (sites mode): ws = WignerSeitzAnalysisModifier( output_displaced = False, per_type_occupancies = True, affine_mapping = ReferenceConfigurationModifier.AffineMapping.ToReference, minimum_image_convention = True ) # Load the reference config from a separate input file. ws.reference = FileSource() # Note: You may have to import FileSource from the ovito.pipeline module. ws.reference.load(ref_file) pipeline.modifiers.append(ws) output_sites = pipeline.compute() def get_defects_from_WS(data): """ Parse data gathered from Wigner-Seitz Analysis """ ws_dict = {} occupancies = data.particles['Occupancy'] site_pos = data.particles['Position'][...] # Get the site types as additional input: site_type = data.particles['Particle Type'] # Calculate total occupancy of every site: total_occupancy = np.sum(occupancies, axis=1) ######## get vacancy information ######## nAvac = np.sum((site_type == 1) & (total_occupancy == 0)) nBvac = np.sum((site_type == 2) & (total_occupancy == 0)) nvac = data.attributes['WignerSeitz.vacancy_count'] print('\nIdenfity %d vacancies, including %d type1 and %d type2'%(nvac,nAvac,nBvac)) if verbose else None print('type1 vacancy site positions:') if verbose else None Avac_pos = site_pos[(site_type == 1) & (total_occupancy == 0)] print(Avac_pos) if verbose else None print('type2 vacancy site positions:') if verbose else None Bvac_pos = site_pos[(site_type == 2) & (total_occupancy == 0)] print(Bvac_pos) if verbose else None ws_dict['nAvac'], ws_dict['nBvac'], ws_dict['Avac_pos'], ws_dict['Bvac_pos'] = nAvac, nBvac, Avac_pos, Bvac_pos ######## get antisite information ######## B2A = np.sum((site_type == 1) & (occupancies[:,1] == 1) & (total_occupancy == 1)) A2B = np.sum((site_type == 2) & (occupancies[:,0] == 1) & (total_occupancy == 1)) print('\nIdenfity %d antisites, including %d type1occupy2 and %d type2occupy1'%(B2A+A2B,A2B,B2A)) if verbose else None print('type2occupy1 site positions:') if verbose else None B2A_pos = site_pos[(site_type == 1) & (occupancies[:,1] == 1) & (total_occupancy == 1)] print(B2A_pos) if verbose else None print('type1occupy2 site positions:') if verbose else None A2B_pos = site_pos[(site_type == 2) & (occupancies[:,0] == 1) & (total_occupancy == 1)] print(A2B_pos) if verbose else None ws_dict['nB2A'], ws_dict['nA2B'], ws_dict['B2A_pos'], ws_dict['A2B_pos'] = B2A, A2B, B2A_pos, A2B_pos ######## get interstitial information ######## int_sites = site_pos[total_occupancy > 1] int_site_types = site_type[total_occupancy > 1] finder = NearestNeighborFinder(np.max(total_occupancy), data) ws_dict['Aint_pos'], ws_dict['Bint_pos'] = [np.empty((0, 3), dtype=np.float64) for n in range(0, 2)] ws_dict['nAint'], ws_dict['nBint'] = 0, 0 # np.array([[]]) for i,pos in enumerate(int_sites): print('\nIntersitial at site',pos,'with site type=',int_site_types[i]) if verbose else None if int_site_types[i] == 1 and pos not in ws_dict['Aint_pos']: ws_dict['Aint_pos'] = np.append(ws_dict['Aint_pos'], np.array([pos]), axis=0) ws_dict['nAint'] += 1 elif int_site_types[i] == 2 and pos not in ws_dict['Bint_pos']: ws_dict['Bint_pos'] = np.append(ws_dict['Bint_pos'], np.array([pos]), axis=0) ws_dict['nBint'] += 1 """ # determine nearest neighbors for each atom print('Atoms at this site') for neigh in finder.find_at(pos): ind = neigh.index print(disp_type[ind],disp_pos[ind])""" return ws_dict print('######## Wigner-Seitz Analysis (Sites) ########') if verbose else None ws_comb_dict['sites'] = get_defects_from_WS(output_sites) # Update Wigner-Seitz analysis (atoms mode): pipeline.modifiers[0].output_displaced = True output_atoms = pipeline.compute() print('######## Wigner-Seitz Analysis (Atoms) ########') if verbose else None ws_comb_dict['atoms'] = get_defects_from_WS(output_atoms) pprint.pprint(ws_comb_dict) if verbose else None # Compare both Wigner-Seitz analyses, return cross-analyzed data defect_dict = { 'nAvac': int(ws_comb_dict['sites']['nAvac']), 'nBvac': int(ws_comb_dict['sites']['nBvac']), 'nA2B': int(ws_comb_dict['sites']['nA2B']), 'nB2A': int(ws_comb_dict['sites']['nB2A']), 'nAint': int(ws_comb_dict['atoms']['nAint']) - int(ws_comb_dict['sites']['nAint']), 'nBint': int(ws_comb_dict['atoms']['nBint']) - int(ws_comb_dict['sites']['nBint']), 'Avac_pos': ws_comb_dict['sites']['Avac_pos'], 'Bvac_pos': ws_comb_dict['sites']['Bvac_pos'], 'A2B_pos': ws_comb_dict['sites']['A2B_pos'], 'B2A_pos': ws_comb_dict['sites']['B2A_pos'], 'Aint_pos': ws_comb_dict['atoms']['Aint_pos'], # np.empty((0, 3), dtype=np.float64), 'Bint_pos': ws_comb_dict['atoms']['Bint_pos'] # np.empty((0, 3), dtype=np.float64) } # something is going wrong, here's some pseudocode """ maybe set original lattice site either way and add to dict for each interstitial (use counts); need to 1. determine distance from interstitial and displaced atom to original lattice site interstitial and displaced atom are both in defect_dict can both be in Aint_pos/Bint_pos, or have one in each original lattice site is in ws_comb_dict['sites'] will be in same Aint_pos/Bint_pos based on location of displaced atom 2a. if int&disp are in the same array, will need to determine which is interstitial based on which one is closer to original lattice site 2b. if int&disp are in different arrays, maybe just treat the same? 3. after determining int&disp, int will remain in interstitial list and disp will be categorized as... either nothing (and will be removed), if distance from site is < ~0.5-0.75 Ang or dumbbell config, if site is roughly shared, distance from site is ~ 0.75-1.75 Ang 4. remove displaced atom either way (can therefore treat this the same, just adding dumbbell to dictionary either way) need to remove disp either way and add dumbbell to dict either way will just add a value if a dumbbell exists, will add empty if not 5. use parsing function to add dumbbell into config if exists """ r_i, r_ij = [], [] og_A_latt_pos, disp_A_fin_pos, db_A_fin_pos = [np.empty((0, 3), dtype=np.float64) for n in range(0, 3)] og_B_latt_pos, disp_B_fin_pos, db_B_fin_pos = [np.empty((0, 3), dtype=np.float64) for n in range(0, 3)] for iA in range(defect_dict['nAint']): #og_A_latt_pos = og_A_latt_pos.append(ws_comb_dict['sites']['Aint_pos'][iA]) pass for iB in range(defect_dict['nBint']): pass """r_i, r_ij = [], [] disp_atom_fin_pos, db_atom_fin_pos = [], [] if defect_dict['nAint'] < defect_dict['Aint_pos'].shape[0]: if defect_dict['nAint'] == 0: # Aint_pos contains only displaced atom, Bint_pos contains interstitial for i in range(defect_dict['Aint_pos'].shape[0]): r_i.append(defect_dict['Aint_pos'][i]) for j in range(ws_comb_dict['sites']['Aint_pos'].shape[0]): r_ij.append(PBC_distance(defect_dict['Aint_pos'][i], ws_comb_dict['sites']['Aint_pos'][j], gan_params)) # need to find index of minimum r_ij value corresponding to first set of r_i values # aka need to slice r_ij list based on [(i*ws_comb_dict['sites']['Aint_pos'].shape[0]):(i+1)*ws_comb_dict['sites']['Aint_pos'].shape[0])] for i in range(len(r_i)): j_start = i*ws_comb_dict['sites']['Aint_pos'].shape[0] j_end = (i + 1)*ws_comb_dict['sites']['Aint_pos'].shape[0] j_min = np.argmin(r_ij[j_start:j_end]) + j_start if r_ij[j_min] <= 0.75: # small displacement disp_atom_fin_pos.append(r_i[i]) elif r_ij[j_min] > 0.75 and r_ij[j_min] <= 1.75: # dumbbell config db_atom_fin_pos.append(r_i[i]) else: pass defect_dict['Aint_pos'] = np.delete(defect_dict['Aint_pos'], r_i[i]) else: # Aint_pos contains both displaced atom and interstitial # need to break in case of interstitial for i in range(defect_dict['Aint_pos'].shape[0]): r_i.append(defect_dict['Aint_pos'][i]) for j in range(ws_comb_dict['sites']['Aint_pos'].shape[0]): r_ij.append(PBC_distance(defect_dict['Aint_pos'][i], ws_comb_dict['sites']['Aint_pos'][j], gan_params)) for i in range(len(r_i)): j_start = i*ws_comb_dict['sites']['Aint_pos'].shape[0] j_end = (i + 1)*ws_comb_dict['sites']['Aint_pos'].shape[0] j_min = np.argmin(r_ij[j_start:j_end]) + j_start if r_ij[j_min] <= 0.75: # small displacement disp_atom_fin_pos.append(r_i[i]) elif r_ij[j_min] > 0.75 and r_ij[j_min] <= 1.75: # dumbbell config db_atom_fin_pos.append(r_i[i]) else: pass defect_dict['Aint_pos'] = np.delete(defect_dict['Aint_pos'], r_i[i]) elif defect_dict['nBint'] < defect_dict['Bint_pos'].shape[0]: if defect_dict['nBint'] == 0: # Bint_pos contains only displaced atom, Aint_pos contains interstitial for i in range(defect_dict['Bint_pos'].shape[0]): r_i.append(defect_dict['Bint_pos'][i]) for j in range(ws_comb_dict['sites']['Bint_pos'].shape[0]): r_ij.append(PBC_distance(defect_dict['Bint_pos'][i], ws_comb_dict['sites']['Bint_pos'][j], gan_params)) for i in range(len(r_i)): j_start = i*ws_comb_dict['sites']['Bint_pos'].shape[0] j_end = (i + 1)*ws_comb_dict['sites']['Bint_pos'].shape[0] j_min = np.argmin(r_ij[j_start:j_end]) + j_start if r_ij[j_min] <= 0.75: # small displacement disp_atom_fin_pos.append(r_i[i]) elif r_ij[j_min] > 0.75 and r_ij[j_min] <= 1.75: # dumbbell config db_atom_fin_pos.append(r_i[i]) else: pass defect_dict['Bint_pos'] = np.delete(defect_dict['Bint_pos'], r_i[i]) else: # Bint_pos contains both displaced atom and interstitial for i in range(defect_dict['Bint_pos'].shape[0]): r_i.append(defect_dict['Bint_pos'][i]) for j in range(ws_comb_dict['sites']['Bint_pos'].shape[0]): r_ij.append(PBC_distance(defect_dict['Bint_pos'][i], ws_comb_dict['sites']['Bint_pos'][j], gan_params)) for i in range(len(r_i)): j_start = i*ws_comb_dict['sites']['Bint_pos'].shape[0] j_end = (i + 1)*ws_comb_dict['sites']['Bint_pos'].shape[0] j_min = np.argmin(r_ij[j_start:j_end]) + j_start if r_ij[j_min] <= 0.75: # small displacement disp_atom_fin_pos.append(r_i[i]) elif r_ij[j_min] > 0.75 and r_ij[j_min] <= 1.75: # dumbbell config db_atom_fin_pos.append(r_i[i]) else: pass defect_dict['Bint_pos'] = np.delete(defect_dict['Bint_pos'], r_i[i])""" # can use now accurate Aint & Bint values # if either Aint or Bint array is longer than the nAint or nBint, from array that is longer, determine which value is closest to original site position # set this closer position to a different variable, remove from int_pos array # if distance is > 0.5ish, add dumbbell position to dictionary """lattice_site_org_pos, disp_atom_fin_pos, db_atom_fin_pos = [], [], [] for i in range(ws_comb_dict['atoms']['Aint_pos'].shape[0]): for j in range(ws_comb_dict['sites']['Aint_pos'].shape[0]): print(ws_comb_dict['atoms']['Aint_pos'][i], ws_comb_dict['sites']['Aint_pos'][j]) print(PBC_distance(ws_comb_dict['atoms']['Aint_pos'][i], ws_comb_dict['sites']['Aint_pos'][j], gan_params)) if (ws_comb_dict['atoms']['Aint_pos'][i] == ws_comb_dict['sites']['Aint_pos'][j]).all(): lattice_site_org_pos.append(ws_comb_dict['atoms']['Aint_pos'][i]) elif PBC_distance(ws_comb_dict['atoms']['Aint_pos'][i], ws_comb_dict['sites']['Aint_pos'][j], gan_params) <= 0.75 and PBC_distance(ws_comb_dict['atoms']['Aint_pos'][i], ws_comb_dict['sites']['Aint_pos'][j], gan_params) > 0.: disp_atom_fin_pos.append(ws_comb_dict['sites']['Aint_pos'][i]) elif PBC_distance(ws_comb_dict['atoms']['Aint_pos'][i], ws_comb_dict['sites']['Aint_pos'][j], gan_params) <= 1.0 and PBC_distance(ws_comb_dict['atoms']['Aint_pos'][i], ws_comb_dict['sites']['Aint_pos'][j], gan_params) > 0.75: db_atom_fin_pos.append(ws_comb_dict['sites']['Aint_pos'][i]) else: defect_dict['Aint_pos'] = ws_comb_dict['atoms']['Aint_pos'] for i in range(ws_comb_dict['atoms']['Bint_pos'].shape[0]): for j in range(ws_comb_dict['sites']['Bint_pos'].shape[0]): print(ws_comb_dict['atoms']['Bint_pos'][i], ws_comb_dict['sites']['Bint_pos'][j]) print(PBC_distance(ws_comb_dict['atoms']['Bint_pos'][i], ws_comb_dict['sites']['Bint_pos'][j], gan_params)) if (ws_comb_dict['atoms']['Bint_pos'][i] == ws_comb_dict['sites']['Bint_pos'][j]).all(): lattice_site_org_pos.append(ws_comb_dict['atoms']['Bint_pos'][i]) elif PBC_distance(ws_comb_dict['atoms']['Bint_pos'][i], ws_comb_dict['sites']['Bint_pos'][j], gan_params) <= 0.75 and PBC_distance(ws_comb_dict['atoms']['Bint_pos'][i], ws_comb_dict['sites']['Bint_pos'][j], gan_params) > 0.: disp_atom_fin_pos.append(ws_comb_dict['sites']['Bint_pos'][i]) elif PBC_distance(ws_comb_dict['atoms']['Bint_pos'][i], ws_comb_dict['sites']['Bint_pos'][j], gan_params) <= 1.0 and PBC_distance(ws_comb_dict['atoms']['Bint_pos'][i], ws_comb_dict['sites']['Bint_pos'][j], gan_params) > 0.75: db_atom_fin_pos.append(ws_comb_dict['sites']['Bint_pos'][i]) else: defect_dict['Bint_pos'] = ws_comb_dict['atoms']['Bint_pos'] defect_dict['nAint'], defect_dict['nBint'] = defect_dict['Aint_pos'].shape[0], defect_dict['Bint_pos'].shape[0]""" #print(og_atom_latt_pos, disp_atom_fin_pos, db_atom_fin_pos) return defect_dict
###################################### # function to determine TDE from a single datafile ######################################
[docs] def get_tde_from_datafile(data_filepath='*_data.csv', e_tol=1.0, ke_cut=45): """ Scans a findTDE *_data.csv file and returns the TDE value for the specified knockout type. """ # read in tde data into structured array tde_data = np.genfromtxt(data_filepath, delimiter=', ', skip_header=1, dtype=[('pseudo', '<U4'), ('atom_type', '<U4'), ('atom_num', '<i4'), ('ke', '<i4'), ('e_fin', '<f8'), ('dE', '<f8')] ) # sort array from lowest KE to highest KE tde_data = np.sort(tde_data, order='ke') # find TDE value from energy difference # first KE corresponding to a dE > e_tol is the TDE in the sorted array tde_value = 0 for i in range(tde_data.shape[0]): if np.isnan(tde_data[i]['dE']) == True: continue elif tde_data[i]['dE'] > e_tol: if tde_data[i]['ke'] <= ke_cut: tde_value = tde_data[i]['ke'] elif tde_data[i]['ke'] > ke_cut: tde_value = ke_cut break else: tde_value = ke_cut return tde_value
###################################### # function to average TDE values for each atom type for a given direction range # function currently assumes rad-induced defect config is Frenkel pair ######################################
[docs] def average_tde_vals(defects_dict, pseudo_keys, lattice_vecs, dir_range=((0, 360), (0, 180))): """ Function to average the TDE values for each atom type over the given range of directions. """ tde_averages = { 'avg': { 'E_d_avg_A': [], 'E_d_avg_B': [] }, 'std': { 'E_d_std_A': [], 'E_d_std_B': [] }, 'weights': { 'E_d_wi_A': [], 'E_d_wi_B': [] } } cnt_A_FP, cnt_B_FP, cnt_complex, cnt_oor, cnt_cutoff = 0, 0, 0, 0, 0 # need variable dtheta, dphi dtheta, dphi = np.deg2rad(7.5), np.deg2rad(7.5) for key in defects_dict.keys(): # check the direction and pass if outside of range if key[-1] == 'S': phi, theta = pseudo_keys[key][0], pseudo_keys[key][1] elif key[-1] == 'L': phi, theta = lat2sph(np.array([pseudo_keys[key]]), lattice_vecs)[0][1], lat2sph(np.array([pseudo_keys[key]]), lattice_vecs)[0][2] else: raise ValueError('Incorrect direction pseudo.') if (phi < dir_range[0][0]) or (phi > dir_range[0][1]) or (theta < dir_range[1][0]) or (theta > dir_range[1][1]): cnt_oor += 1 continue # check the resultant defect type if (defects_dict[key]['nAint'] > 0) and (defects_dict[key]['nAvac'] > 0): defect_type = 'A' cnt_A_FP += 1 elif (defects_dict[key]['nBint'] > 0) and (defects_dict[key]['nBvac'] > 0): defect_type = 'B' cnt_B_FP += 1 else: #print('{} --> Other type: nAint = {}, nAvac = {}, nBint = {}, nBvac = {}'.format(key, defects_dict[key]['nAint'], defects_dict[key]['nAvac'], defects_dict[key]['nBint'], defects_dict[key]['nBvac'])) #print(defects_dict[key]) cnt_complex += 1 continue # categorize TDE values by the defect type tde_averages['avg'][f'E_d_avg_{defect_type}'].append(defects_dict[key]['E_d']) tde_averages['weights'][f'E_d_wi_{defect_type}'].append(np.sin(np.deg2rad(theta))*dtheta*dphi) # find standard deviation of the TDE values for each defect type for key in tde_averages['std'].keys(): tde_averages['std'][key] = np.std(tde_averages['avg'][key.replace('std', 'avg')], dtype=np.float64) # average the TDE values for each defect type for key in tde_averages['avg'].keys(): # standard average # tde_averages['avg'][key] = np.mean(tde_averages['avg'][key], dtype=np.float64) # weighted average tde_averages['avg'][key] = np.average(tde_averages['avg'][key], weights=tde_averages['weights'][key.replace('avg', 'wi')]) del tde_averages['weights'] print('A FPs:', cnt_A_FP, '\tB FPs:', cnt_B_FP, '\tOther:', cnt_complex, '\tOut of range:', cnt_oor) return tde_averages
###################################### # can then take the code i started to make previously to analyze this info # maybe need to change how it is stored first in the original function ######################################
[docs] def parse_defect_properties(defects_dict, atom_types=['Ga', 'N']): """ Function to analyze the defect properties for each calculation contained in a dictionary. """ # create initial DataFrame to emulate table defects_df = pd.DataFrame(data=defects_dict) # add empty rows to Dataframe for additional data defects_df.loc['Defect Type'] = pd.Series([None for n in range(defects_df.columns.shape[0])]) defects_df.loc['d_sep'] = pd.Series([None for n in range(defects_df.columns.shape[0])]) # iterate through columns (calculation directions) for i in range(defects_df.columns.shape[0]): vacancies_list = ['V_{}'.format(atom_types[0]) for v in range(defects_df[defects_df.columns[i]].loc['nAvac'])] + \ ['V_{}'.format(atom_types[1]) for v in range(defects_df[defects_df.columns[i]].loc['nBvac'])] antisites_list = ['{}_{}'.format(atom_types) for a in range(defects_df[defects_df.columns[i]].loc['nA2B'])] + \ ['{}_{}'.format(atom_types[::-1]) for a in range(defects_df[defects_df.columns[i]].loc['nB2A'])] interstitials_list = ['{}_i'.format(atom_types[0]) for t in range(defects_df[defects_df.columns[i]].loc['nAint'])] + \ ['{}_i'.format(atom_types[1]) for t in range(defects_df[defects_df.columns[i]].loc['nBint'])] defects_df[defects_df.columns[i]].loc['Defect Type'] = ' + '.join(vacancies_list+antisites_list+interstitials_list) # could determine O or T by having a list of possible O and T sites, determine distance from each via displacement vector, choose lowest distance if defects_df[defects_df.columns[i]].loc['nAvac'] == 1 and defects_df[defects_df.columns[i]].loc['nAint'] == 1: defects_df[defects_df.columns[i]].loc['d_sep'] = PBC_distance(defects_df[defects_df.columns[i]].loc['Aint_pos'], defects_df[defects_df.columns[i]].loc['Avac_pos'], gan_params) elif defects_df[defects_df.columns[i]].loc['nBvac'] == 1 and defects_df[defects_df.columns[i]].loc['nBint'] == 1: defects_df[defects_df.columns[i]].loc['d_sep'] = PBC_distance(defects_df[defects_df.columns[i]].loc['Bint_pos'], defects_df[defects_df.columns[i]].loc['Bvac_pos'], gan_params) # remove unnecessary info used in analysis for the purposes of displaying defects_df = defects_df.drop(['nAvac', 'nBvac', 'nA2B', 'nB2A', 'nAint', 'nBint']) # sort DataFrame defects_df.sort_index(inplace=True) # would be good to include information such as the knockout atom and the Ed value return defects_df
#####command: ovitos xx.py if __name__ == "__main__": FILES_LOC = os.getcwd() # os.path.dirname(__file__) run_type, knockout_atom = 'vasp', 'ga34' if run_type == 'vasp': defect_files = find_contcars(FILES_LOC, pseudo=f'*{knockout_atom}*') elif run_type == 'lammps': defect_files = find_dumpfiles(FILES_LOC, pseudo=f'*{knockout_atom}*') # test with 10*S_ga34* # pprint.pprint(defect_files) # read in VASP and LAMMPS perfect structure information if run_type == 'vasp': vasp_ref_file = os.path.join(FILES_LOC, 'inp', 'POSCAR') elif run_type == 'lammps': vasp_ref_file, lmp_ref_file = os.path.join(FILES_LOC, 'inp', 'POSCAR'), os.path.join(FILES_LOC, 'inp', 'read_data_perfect.lmp') pos_f = open(vasp_ref_file, 'r') pos_lines = pos_f.readlines() pos_f.close() # open POSCAR file and read lattice vector lines into a list ai = [pos_lines[2][1:-1], pos_lines[3][1:-1], pos_lines[4][1:-1]] # convert lattice vectors line from POSCAR file from Fortran to a list lat_vec_line = ff.FortranRecordReader('(3F22.16)') vasp_lattice_vecs = np.array([lat_vec_line.read(ai[j]) for j in range(len(ai))]) vasp_params = np.array([np.linalg.norm(vasp_lattice_vecs[0, :]), np.linalg.norm(vasp_lattice_vecs[1, :]), np.linalg.norm(vasp_lattice_vecs[2, :])]) # add analysis information for each direction to a dict defect_configs_dict = {} for i in range(defect_files.shape[0]): if run_type == 'vasp': calc_desc, calc_tde = defect_files[i].split('/')[-4], int(defect_files[i].split('/')[-3][:-2]) calc_pseudo, calc_atom_info = calc_desc.split('_')[0], calc_desc.split('_')[1] # get TDE value for direction from data file pseudo_datafile = os.path.abspath(os.path.join(defect_files[i], os.pardir, os.pardir, os.pardir, calc_pseudo+'_'+calc_atom_info+'_data.csv')) pseudo_tde_value = get_tde_from_datafile(pseudo_datafile, e_tol=1.0, ke_cut=45) elif run_type == 'lammps': calc_desc, calc_tde = defect_files[i].split('/')[-3], int(defect_files[i].split('/')[-2][:-2]) calc_pseudo, calc_atom_info = calc_desc.split('_')[0], calc_desc.split('_')[1] # get TDE value for direction from data file pseudo_datafile = os.path.abspath(os.path.join(defect_files[i], os.pardir, os.pardir, calc_pseudo+'_'+calc_atom_info+'_data.csv')) pseudo_tde_value = get_tde_from_datafile(pseudo_datafile, e_tol=1.0, ke_cut=100) # print(calc_desc, ': ', calc_tde, ' eV\t (TDE: ', pseudo_tde_value, ' eV)' sep='') # perform defect analysis if calc_tde == pseudo_tde_value: defect_configs_dict[calc_pseudo] = defect_analysis(defect_files[i], vasp_ref_file) defect_configs_dict[calc_pseudo]['E_d'] = pseudo_tde_value elif calc_tde != pseudo_tde_value: # potentially need to add cutoff handling here if pseudo_tde_value >= 45: print('cutoff needed') continue else: continue print('############################ OVITO WS ANALYSIS SUMMARY ############################') # pprint.pprint(defect_configs_dict) if run_type == 'vasp': vasp_pseudo_keys = pseudo_keys_from_file(pseudo_keyfile=os.path.join(FILES_LOC, 'gan_vasp_pseudo_keys.csv')) elif run_type == 'lammps': lmp_pseudo_keys = pseudo_keys_from_file(pseudo_keyfile=os.path.join(FILES_LOC, 'gan_lmp_pseudo_keys.csv')) pprint.pprint(average_tde_vals(defect_configs_dict, vasp_pseudo_keys, vasp_lattice_vecs, dir_range=((30, 150), (0, 180)))) # defect_configs_df = parse_defect_properties(defect_configs_dict, atom_types=['Ga', 'N']).T # pprint.pprint(defect_configs_df)