webmo 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
webmo/util.py ADDED
@@ -0,0 +1,358 @@
1
+ import numpy as np
2
+
3
+ def xyz_from_name(name, include_atomic_number=False):
4
+ """Obtains an XYZ-formatted geometry for the molecule name via PubChem lookup.
5
+
6
+ Arguments:
7
+ name(str): The name of the molecule.
8
+ include_atomic_number(bool): Whether to include the atomic number as well in the XYZ output (e.g. for GAMESS formatted Cartesian coordinates)
9
+
10
+ Returns:
11
+ A string containing the molecule's XYZ-formatted geometry.
12
+ """
13
+ import pubchempy as pcp
14
+
15
+ results = pcp.get_compounds(name, 'name', record_type='3d')
16
+ if (len(results) == 0):
17
+ raise ValueError("Specified molecule could not be found in PubChem")
18
+
19
+ record = results[0]
20
+ geom = ""
21
+ for atom in record.atoms:
22
+ element = atom.element
23
+ number = int(atom.number)
24
+ x = float(atom.x)
25
+ y = float(atom.y)
26
+ z = float(atom.z)
27
+ if include_atomic_number:
28
+ geom += f"{element}\t{number:d}\t{x:f}\t{y:f}\t{z:f}\n"
29
+ else:
30
+ geom += f"{element}\t{x:f}\t{y:f}\t{z:f}\n"
31
+
32
+ return geom
33
+
34
+ def get_energy(results, include_units=False):
35
+ """Returns the molecular energy from the job results. This should correspond to the ground state energy
36
+ at molecule at the requested level of theory.
37
+
38
+ Requires WebMO 25 or higher.
39
+
40
+ Arguments:
41
+ results(dict): A dictionary of job results, obtained from get_job_results.
42
+ include_units(bool): Whether to include the units with the returned energy (e.g. 1.5 vs. "1.5 Hartree")
43
+
44
+ Returns:
45
+ The molecular energy.
46
+ """
47
+ properties = results['properties']
48
+
49
+ if not 'method_energy_name' in properties.keys():
50
+ raise RuntimeError('Cannot locate "method_energy_name" in results')
51
+ method_energy_name = properties['method_energy_name'].lower().replace(' ', '_') + '_energy'
52
+
53
+ found = False
54
+ #also check related key names, since Gaussian prepends some things occasionally
55
+ for key in [method_energy_name, 'r' + method_energy_name, 'u' + method_energy_name, 'ro' + method_energy_name]:
56
+ if key in properties:
57
+ energy = properties[key]['value']
58
+ units = properties[key]['units']
59
+ found = True
60
+
61
+ if not found:
62
+ raise RuntimeError('Could not locate "' + method_energy_name + '" in results')
63
+
64
+ if include_units:
65
+ energy = f"{energy} {units}"
66
+
67
+ return energy
68
+
69
+ def get_energies(results, include_units=False):
70
+ """Returns a dictionary of available energies from the job results.
71
+
72
+ Arguments:
73
+ results(dict): A dictionary of job results, obtained from get_job_results.
74
+ include_units(bool): Whether to include the units with the returned energy (e.g. 1.5 vs. "1.5 Hartree")
75
+
76
+ Returns:
77
+ A dictionary of available energies.
78
+ """
79
+ properties = results['properties']
80
+ energies = {}
81
+
82
+ for key in properties.keys():
83
+ index = key.find('_energy')
84
+
85
+ if key.endswith('_energy'):
86
+ index = key.find('_energy')
87
+ energy_type = key[:index]
88
+
89
+ if include_units:
90
+ energies[energy_type] = f"{properties[key]['value']} {properties[key]['units']}"
91
+ else:
92
+ energies[energy_type] = properties[key]['value']
93
+
94
+ return energies
95
+
96
+ def get_geometry(results):
97
+ """Returns the XYZ-formatted final geometry from the job results.
98
+
99
+ Arguments:
100
+ results(dict): A dictionary of job results, obtained from get_job_results.
101
+
102
+ Returns:
103
+ The XYZ-formatted final geometry as a string.
104
+ """
105
+
106
+
107
+ geom = ""
108
+ index = 0
109
+ for element in results['symbols']:
110
+
111
+ x = results['geometry'][index]
112
+ y = results['geometry'][index + 1]
113
+ z = results['geometry'][index + 2]
114
+
115
+ geom += f"{element}\t{x:f}\t{y:f}\t{z:f}\n"
116
+ index += 3
117
+
118
+ return geom
119
+
120
+ def get_stoichiometry(results):
121
+ """Returns the stoichiometry from the job results.
122
+
123
+ Arguments:
124
+ results(dict): A dictionary of job results, obtained from get_job_results.
125
+
126
+ Returns:
127
+ The stoichiometry as a string.
128
+ """
129
+
130
+ properties = results['properties']
131
+ return properties['stoichiometry']
132
+
133
+ def get_dipole_moment(results):
134
+ """Returns the dipole moment from the job results.
135
+
136
+ Arguments:
137
+ results(dict): A dictionary of job results, obtained from get_job_results.
138
+
139
+ Returns:
140
+ The magnitude of the dipole moment.
141
+ """
142
+
143
+ from math import sqrt
144
+
145
+ properties = results['properties']
146
+ dipole = properties['dipole_moment']
147
+ mag = sqrt(dipole[0]**2 + dipole[1]**2 + dipole[2]**2)
148
+
149
+ return mag
150
+
151
+ def get_property(results, property_name):
152
+ """Returns an arbitrary calculated property from the job results.
153
+
154
+ Arguments:
155
+ results(dict): A dictionary of job results, obtained from get_job_results.
156
+ property_name(str): The name of the desired property
157
+
158
+ Returns:
159
+ The value of the calculated property (scalar or dictionary).
160
+ """
161
+
162
+ properties = results['properties']
163
+ return properties[property_name]
164
+
165
+
166
+ def get_bond_length(results, index1, index2):
167
+ """Returns the specified bond length between two atoms from the job results OR an xyz-formatted string.
168
+
169
+ Arguments:
170
+ dict(results): A dictionary of job results obtained from get_job_results OR an xyz-formatted string.
171
+ index1(int): The one-based index of the first atom of the bonded pair
172
+ index2(int): The one-based index of the second atom of the bonded pair
173
+
174
+ Returns:
175
+ The calculated bond length.
176
+ """
177
+
178
+ if not isinstance(results, dict):
179
+ geom = _xyz_to_array(results)
180
+ else:
181
+ geom = results['geometry']
182
+
183
+ #convert to 0 index
184
+ index1 -= 1
185
+ index2 -= 1
186
+
187
+ atom1 = np.array(geom[index1*3:index1*3 + 3])
188
+ atom2 = np.array(geom[index2*3:index2*3 + 3])
189
+
190
+ return np.linalg.norm(atom1 - atom2)
191
+
192
+ def get_bond_angle(results, index1, index2, index3):
193
+ """Returns the specified bond angle between three atoms from the job results OR an xyz-formatted string.
194
+
195
+ Arguments:
196
+ dict(results): A dictionary of job results obtained from get_job_results OR an xyz-formatted string.
197
+ index1(int): The one-based index of the first atom
198
+ index2(int): The one-based index of the second atom
199
+ index3(int): The one-based index of the third atom
200
+
201
+ Returns:
202
+ The calculated bond angle.
203
+ """
204
+
205
+ if not isinstance(results, dict):
206
+ geom = _xyz_to_array(results)
207
+ else:
208
+ geom = results['geometry']
209
+
210
+ #convert to 0 index
211
+ index1 -= 1
212
+ index2 -= 1
213
+ index3 -= 1
214
+
215
+ atom1 = np.array(geom[index1*3:index1*3 + 3])
216
+ atom2 = np.array(geom[index2*3:index2*3 + 3])
217
+ atom3 = np.array(geom[index3*3:index3*3 + 3])
218
+
219
+ vec1 = atom1 - atom2
220
+ vec2 = atom3 - atom2
221
+
222
+ vec1 /= np.linalg.norm(vec1)
223
+ vec2 /= np.linalg.norm(vec2)
224
+
225
+ return np.arccos(np.dot(vec1,vec2))*180./np.pi
226
+
227
+ def get_dihedral_angle(results, index1, index2, index3, index4):
228
+ """Returns the specified dihedral angle between four atoms from the job results OR an xyz-formatted string.
229
+
230
+ Arguments:
231
+ dict(results): A dictionary of job results obtained from get_job_results OR an xyz-formatted string.
232
+ index1(int): The one-based index of the first atom
233
+ index2(int): The one-based index of the second atom
234
+ index3(int): The one-based index of the third atom
235
+ index4(int): The one-based index of the fourth atom
236
+
237
+ Returns:
238
+ The calculated dihedral angle.
239
+ """
240
+
241
+ if not isinstance(results, dict):
242
+ geom = _xyz_to_array(results)
243
+ else:
244
+ geom = results['geometry']
245
+
246
+ #convert to 0 index
247
+ index1 -= 1
248
+ index2 -= 1
249
+ index3 -= 1
250
+ index4 -= 1
251
+
252
+ atom1 = np.array(geom[index1*3:index1*3 + 3])
253
+ atom2 = np.array(geom[index2*3:index2*3 + 3])
254
+ atom3 = np.array(geom[index3*3:index3*3 + 3])
255
+ atom4 = np.array(geom[index4*3:index4*3 + 3])
256
+
257
+ b1 = atom1 - atom2
258
+ b2 = atom2 - atom3
259
+ b3 = atom3 - atom4
260
+
261
+ b1 /= np.linalg.norm(b1)
262
+ b2 /= np.linalg.norm(b2)
263
+ b3 /= np.linalg.norm(b3)
264
+
265
+ n1 = np.cross(b1, b2)
266
+ n2 = np.cross(b2, b3)
267
+ m = np.cross(n1, b2)
268
+
269
+ x = np.dot(n1, n2)
270
+ y = np.dot(m, n2)
271
+
272
+ return np.arctan2(y, x)*180./np.pi
273
+
274
+ def get_option_matching(template, variable, desired_value_aliases, ignore_missing_variables=False):
275
+ """Returns the best matching template variable option for the specified variable.
276
+
277
+ This can be useful when an engine uses non-standard names for options and you want to use a
278
+ more descriptive name (like that shown in the dropdown) rather than the corresponding value. It attempts
279
+ to match either the corresponding option value (if it exists) OR the description.
280
+
281
+ For example:
282
+ get_option_matching(template, 'basisSet', '6-31G(d)')
283
+ will return the value:
284
+ "N31 NGAUSS=6 NDFUNC=1"
285
+ since it matches the corresponding option description "Routine: 6-31G(d)" in the dropdown.
286
+
287
+ Arguments:
288
+ template(dict): The desired job template (from get_templates)
289
+ variable(str): The template variable
290
+ desired_value_aliases(list): A list of potentially matching values, or aliases thereof; a single string is also acceptable.
291
+ ignore_missing_variables(bool, opt): Whether to raise an exception if the template variable does not exist in the template
292
+
293
+ Returns:
294
+ The best matching corresponding variable value
295
+ """
296
+
297
+ def ends_with_whole_word(text, target_word):
298
+ # Check if it ends with the word
299
+ if not text.endswith(target_word):
300
+ return False
301
+ # Get the start index of the target word in the text
302
+ idx = len(text) - len(target_word)
303
+ # If it's at the very beginning of the string, it's a whole word
304
+ if idx == 0:
305
+ return True
306
+ # Check if the character just before our word is a space or a colon
307
+ return text[idx - 1].isspace() or text[idx - 1] == ':'
308
+
309
+ #immediately return if the desired value is blank (for some reason)
310
+ if isinstance(desired_value_aliases, str) and not desired_value_aliases.strip():
311
+ return ""
312
+
313
+ #normalize any scalar arguement ot a list
314
+ if not isinstance(desired_value_aliases, list):
315
+ desired_value_aliases = [desired_value_aliases]
316
+ #immediately return if this variable does not exist in this particular template
317
+ if not variable in template['variables']:
318
+ if ignore_missing_variables:
319
+ return ""
320
+ else:
321
+ raise ValueError(f"Could not locate variable {variable} in template")
322
+ #ensure that the template variable is a 'dropdown' type
323
+ if template['variables'][variable]['type'] != 'dropdown':
324
+ raise ValueError(f"Template variable {variable} is not of type dropdown")
325
+
326
+ options = template['variables'][variable]['options']
327
+ for desired_value in desired_value_aliases:
328
+ for key, value in options.items():
329
+ #check if the specified value exists as value in the options dictionary
330
+ if value.casefold() == desired_value.casefold():
331
+ return value
332
+ #otherwise check if it matches the end of any key in the dictionary
333
+ if ends_with_whole_word(key.casefold(), desired_value.casefold()):
334
+ return options[key]
335
+
336
+ #not found!
337
+ raise ValueError(f"Could not locate option {desired_value_aliases} for template variable {variable}")
338
+
339
+
340
+ def _xyz_to_array(xyz):
341
+ xyz = xyz.strip()
342
+
343
+ try:
344
+ #throw exception if NOT a number, indicating that our XYZ file has no header
345
+ tokens = xyz.split()
346
+ int(tokens[0])
347
+ #this XYZ formatted string is proper, containing a header that must be removed
348
+ xyz = xyz.split("\n",2)[2]
349
+ except ValueError:
350
+ pass
351
+
352
+ #process the actual geometry of the XYZ file, converting into an array
353
+ geom = xyz.split()
354
+ #remove the element names, leaving only an array of positions
355
+ del geom[0::4]
356
+
357
+ #convert to an array of floats and return
358
+ return np.array(geom,dtype=float)