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/__init__.py +2 -0
- webmo/gui.py +193 -0
- webmo/spectrum.py +451 -0
- webmo/util.py +358 -0
- webmo/webmo_rest.py +903 -0
- webmo-1.6.0.dist-info/METADATA +35 -0
- webmo-1.6.0.dist-info/RECORD +10 -0
- webmo-1.6.0.dist-info/WHEEL +5 -0
- webmo-1.6.0.dist-info/licenses/LICENSE +21 -0
- webmo-1.6.0.dist-info/top_level.txt +1 -0
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)
|