-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathASM_NBO.py
More file actions
361 lines (314 loc) · 11 KB
/
ASM_NBO.py
File metadata and controls
361 lines (314 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
import argparse
import os
import sys
from scm.plams import init, finish, KFReader, Settings, ADFJob, Atom, Molecule
def main():
"""
Main interface that reads the file and retrieve all useful geoms etc.
Then rewrite geometries in a usable formatter_class
Prep all NBO computations
Run ADF on each computation
"""
# Retrieve command line values
args = get_input_arguments()
input_file = args["input_file"]
output_file = args["output_file"]
# Init PLAMS, Setting up a plams.$PID directory in the working dir
init()
# Retrieve settings from input file
settings = get_settings(args)
geometries = IRC_coordinates_from_t21(input_file)
natoms = number_of_atoms(input_file)
# Prep a bunch of NBO computations
NBO_jobs = [prepare_NBO_computation(geom, settings) for geom in geometries]
# Run each job in parallel as managed by plams
for job in NBO_jobs:
job.run()
# NBO_values is a list of list of charges
NBO_values = [
extract_NBO_charges(job._filenames.get("out"), natoms) for job in NBO_jobs
]
# Write NBO data
print_NBO_charges_to_file(NBO_values, output_file)
# Close plams session
finish()
def IRC_coordinates_to_xyz_file(filename, geometries):
"""
Export coordinates in geometries table to a file 'filename'
straight in the working directory
"""
with open(filename, mode="w+") as XYZ:
for i in range(0, len(geometries)):
XYZ.write("New molecule\n")
for j in range(0, len(geometries[i])):
for k in range(0, len(geometries[i][j])):
XYZ.write(str(geometries[i][j][k]) + " ")
XYZ.write("\n")
XYZ.write("\n\n")
return
def geometry_to_molecule(geometry):
"""
Convert a list of XYZ coordinates to a Molecule object
"""
mol = Molecule()
for i in range(0, len(geometry)):
mol.add_atom(
Atom(
symbol=geometry[i][0],
coords=(geometry[i][1], geometry[i][2], geometry[i][3]),
)
)
return mol
def IRC_coordinates_from_t21(input_file):
"""
Extract all data from a TAPE21 file.
"""
# Read TAPE21 and get all useful data
t21 = KFReader(input_file)
# Number of atoms: 7
natoms = t21.read("Geometry", "nr of atoms")
# atom types as indexes: [1, 2, 2, 3, 3, 4, 3]
aatoms = t21.read("Geometry", "fragment and atomtype index")[natoms:]
# Atom symbols as list: ['C', 'O', 'H', 'B']
xatoms = str(t21.read("Geometry", "atomtype")).split()
# Actual list of atoms as used in geometry: ['C', 'O', 'O', 'H', 'H', 'B', 'H']
satoms = [
xatoms[aatoms[order - 1] - 1]
for order in t21.read("Geometry", "atom order index")[:natoms]
]
nstep_fw = t21.read("IRC_Forward", "CurrentPoint")
nstep_bw = t21.read("IRC_Backward", "CurrentPoint")
geometries_fw = t21.read("IRC_Forward", "xyz")[0 : nstep_fw * natoms * 3]
geometries_bw = t21.read("IRC_Backward", "xyz")[0 : nstep_bw * natoms * 3]
geometries_init = t21.read("IRC", "xyz")
# Reformat geometries into a long list of geometries for each step
geometries_bw = coordinates_from_list(geometries_bw, natoms)
geometries_fw = coordinates_from_list(geometries_fw, natoms)
geometries_init = coordinates_from_list(geometries_init, natoms)
geometries_fw.reverse()
geometries = geometries_fw + geometries_init + geometries_bw
return [
[
[s, mol[0], mol[1], mol[2]]
for i, (s, mol) in enumerate(zip(satoms, molecule))
]
for molecule in geometries
]
def coordinates_from_list(coordinates_list, natoms):
"""
Rewrites a list into a list of lists of lists:
[x1,y1,z1,x2,y2,z2,x'1,y'1,z'1,x'2,y'2,z'2] becomes:
[ [ [x1,y1,z1], [x2,y2,z2]], [ [x'1,y'1,z'1], [x'2,y'2,z'2] ] ]
which can be acessed as:
list[moleculeNumber][atomNumber][coordinateNumber]
"""
# list = [x1,y1,z1,x2,y2,z2,x'1,y'1,z'1,x'2,y'2,z'2]
coordinates_list = list(list_chunks(coordinates_list, 3))
# [[x1,y1,z1],[x2,y2,z2],[x'1,y'1,z'1],[x'2,y'2,z'2]]
coordinates_list = list(list_chunks(coordinates_list, natoms))
# [ [[x1,y1,z1],[x2,y2,z2]], [[x'1,y'1,z'1],[x'2,y'2,z'2]]]
return coordinates_list
def list_chunks(list, n):
"""
Generator that splits a list in chunks of size n
/!\ Does not care about the end of the list if len(list) is not a multiple of n
"""
for i in range(0, len(list), n):
yield list[i : i + n]
def prepare_NBO_computation(geometry, runparameters):
"""
Taking a geometry and a set of ADF parameters (Basis set, Functional, ZORA, etc)
Add the NBO Necessary keywords and returns the plams job
"""
nbo_script = "\n".join(
[
'"$ADFBIN/adfnbo" << eor',
"write",
"spherical",
"nbo-analysis",
"eor",
"",
'"$ADFBIN/gennbo6" FILE47',
"",
'"$ADFBIN/adfnbo" <<eor',
"spherical",
"fock",
"read",
"end input",
"eor",
]
)
runparameters.runscript.post = nbo_script
job = ADFJob(
name="NBO_Computation",
settings=runparameters,
molecule=geometry_to_molecule(geometry),
)
return job
def extract_NBO_charges(output, natoms):
"""
extract NBO Charges parsing the outFile
"""
# Initialize charges list
charges = []
with open(output, mode="r") as outFile:
line = "Foobar line"
while line:
line = outFile.readline()
if "Summary of Natural Population Analysis:" in line:
# We have the table we want for the charges
# Read five lines to remove the header:
# Summary of Natural Population Analysis:
#
# Natural Population
# Natural ---------------------------------------------
# Atom No Charge Core Valence Rydberg Total
# ----------------------------------------------------------------
for i in range(0, 5):
outFile.readline()
# Then we read the actual table:
for i in range(0, natoms):
# Each line follow the header with the form:
# C 1 0.92349 1.99948 3.03282 0.04422 5.07651
line = outFile.readline()
line = line.split()
charges.append(line[2])
# We have reached the end of the table, we can break the while loop
break
return charges
def print_NBO_charges_to_file(charges_list, file):
"""
Export NBO charges to a file that one can import in a spreadsheet or gnuplot
"""
with open(file, mode="w+") as output_file:
for i in range(0, len(charges_list)):
output_file.write(" ".join(charges_list[i]) + "\n")
def number_of_atoms(input_file):
"""
Extracts natoms from TAPE21
"""
t21 = KFReader(input_file)
return t21.read("Geometry", "nr of atoms")
def get_input_arguments():
"""
Check command line options and accordingly set computation parameters
"""
parser = argparse.ArgumentParser(
description=help_description(), epilog=help_epilog()
)
parser.formatter_class = argparse.RawDescriptionHelpFormatter
parser.add_argument(
"input_file", type=str, nargs=1, help="TAPE21 Containg IRC path"
)
parser.add_argument(
"output_file",
type=str,
nargs=1,
help="Output file in which to print the NBO charges",
)
parser.add_argument(
"-f",
"--functional",
type=str,
nargs="?",
default="BLYP-D3",
help="Functional used for the computation, as BLYP-D3 or BP86\n"
"Hyphen will split into functional/dispersion parts when applicable",
)
parser.add_argument(
"-r",
"--relativistic",
type=str,
nargs="?",
default="None",
help="Relativistic effects: Scalar, Spin-Orbit or None (default)",
)
parser.add_argument(
"-b",
"--basisset",
type=str,
nargs="?",
default="DZP",
help="The basis set to use for all atoms",
)
parser.add_argument(
"-c",
"--frozencore",
type=str,
nargs="?",
default="None",
help="Frozen core to use: None (Default), Small, Large",
)
parser.add_argument(
"-i",
"--integrationquality",
type=str,
nargs="?",
default="Good",
help="Numerical Integration Quality. Default: Good",
)
try:
args = parser.parse_args()
except argparse.ArgumentError as error:
print(str(error)) # Print something like "option -a not recognized"
sys.exit(2)
# Get values from parser
values = dict.fromkeys(
[
"input_file",
"output_file",
"functional",
"dispersion",
"relativistic",
"basisset",
"frozencore",
"integrationquality",
]
)
values["input_file"] = os.path.basename(args.input_file[0])
values["output_file"] = os.path.basename(args.output_file[0])
functional = args.functional.split("-")
values["functional"] = functional[0]
if len(functional) > 1:
if functional[1] == "GD3" or functional[1] == "D3":
values["dispersion"] = "Grimme3"
elif functional[1] == "GD3BJ":
values["dispersion"] = "Grimme3 BJDAMP"
else:
values["dispersion"] = None
values["relativistic"] = args.relativistic
values["basisset"] = args.basisset
values["frozencore"] = args.frozencore
values["integrationquality"] = args.integrationquality
return values
def get_settings(values):
"""
Retrieve settings from command line, and set them up
"""
settings = Settings()
settings.input.XC.GGA = values["functional"]
if values["dispersion"] is not None:
settings.input.XC.DISPERSION = values["dispersion"]
settings.input.BASIS.type = values["basisset"]
settings.input.BASIS.core = values["frozencore"]
settings.input.BASIS.createoutput = "None"
settings.input.NumericalQuality = values["integrationquality"]
settings.input.RELATIVISTIC = values["relativistic"] + " ZORA"
settings.input.AOMAT2FILE = ""
settings.input.SAVE = "TAPE15"
settings.input.FULLFOCK = ""
settings.input.NOPRINT = "LOGFILE"
settings.input.SYMMETRY = "NOSYM"
return settings
def help_description():
"""
Returns description of program for help message
"""
return "Help Description // To fill"
def help_epilog():
"""
Returns additionnal help message
"""
return "Help epilog // To Fill"
if __name__ == "__main__":
main()