digichem-core 6.0.0rc1__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.
- digichem/__init__.py +75 -0
- digichem/basis.py +116 -0
- digichem/config/README +3 -0
- digichem/config/__init__.py +5 -0
- digichem/config/base.py +321 -0
- digichem/config/locations.py +14 -0
- digichem/config/parse.py +90 -0
- digichem/config/util.py +117 -0
- digichem/data/README +4 -0
- digichem/data/batoms/COPYING +18 -0
- digichem/data/batoms/LICENSE +674 -0
- digichem/data/batoms/README +2 -0
- digichem/data/batoms/__init__.py +0 -0
- digichem/data/batoms/batoms-renderer.py +351 -0
- digichem/data/config/digichem.yaml +714 -0
- digichem/data/functionals.csv +15 -0
- digichem/data/solvents.csv +185 -0
- digichem/data/tachyon/COPYING.md +5 -0
- digichem/data/tachyon/LICENSE +30 -0
- digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
- digichem/data/vmd/common.tcl +468 -0
- digichem/data/vmd/generate_combined_orbital_images.tcl +70 -0
- digichem/data/vmd/generate_density_images.tcl +45 -0
- digichem/data/vmd/generate_dipole_images.tcl +68 -0
- digichem/data/vmd/generate_orbital_images.tcl +57 -0
- digichem/data/vmd/generate_spin_images.tcl +66 -0
- digichem/data/vmd/generate_structure_images.tcl +40 -0
- digichem/datas.py +14 -0
- digichem/exception/__init__.py +7 -0
- digichem/exception/base.py +133 -0
- digichem/exception/uncatchable.py +63 -0
- digichem/file/__init__.py +1 -0
- digichem/file/base.py +364 -0
- digichem/file/cube.py +284 -0
- digichem/file/fchk.py +94 -0
- digichem/file/prattle.py +277 -0
- digichem/file/types.py +97 -0
- digichem/image/__init__.py +6 -0
- digichem/image/base.py +113 -0
- digichem/image/excited_states.py +335 -0
- digichem/image/graph.py +293 -0
- digichem/image/orbitals.py +239 -0
- digichem/image/render.py +617 -0
- digichem/image/spectroscopy.py +797 -0
- digichem/image/structure.py +115 -0
- digichem/image/vmd.py +826 -0
- digichem/input/__init__.py +3 -0
- digichem/input/base.py +78 -0
- digichem/input/digichem_input.py +500 -0
- digichem/input/gaussian.py +140 -0
- digichem/log.py +179 -0
- digichem/memory.py +166 -0
- digichem/misc/__init__.py +4 -0
- digichem/misc/argparse.py +44 -0
- digichem/misc/base.py +61 -0
- digichem/misc/io.py +239 -0
- digichem/misc/layered_dict.py +285 -0
- digichem/misc/text.py +139 -0
- digichem/misc/time.py +73 -0
- digichem/parse/__init__.py +13 -0
- digichem/parse/base.py +220 -0
- digichem/parse/cclib.py +138 -0
- digichem/parse/dump.py +253 -0
- digichem/parse/gaussian.py +130 -0
- digichem/parse/orca.py +96 -0
- digichem/parse/turbomole.py +201 -0
- digichem/parse/util.py +523 -0
- digichem/result/__init__.py +6 -0
- digichem/result/alignment/AA.py +114 -0
- digichem/result/alignment/AAA.py +61 -0
- digichem/result/alignment/FAP.py +148 -0
- digichem/result/alignment/__init__.py +3 -0
- digichem/result/alignment/base.py +310 -0
- digichem/result/angle.py +153 -0
- digichem/result/atom.py +742 -0
- digichem/result/base.py +258 -0
- digichem/result/dipole_moment.py +332 -0
- digichem/result/emission.py +402 -0
- digichem/result/energy.py +323 -0
- digichem/result/excited_state.py +821 -0
- digichem/result/ground_state.py +94 -0
- digichem/result/metadata.py +644 -0
- digichem/result/multi.py +98 -0
- digichem/result/nmr.py +1086 -0
- digichem/result/orbital.py +647 -0
- digichem/result/result.py +244 -0
- digichem/result/soc.py +272 -0
- digichem/result/spectroscopy.py +514 -0
- digichem/result/tdm.py +267 -0
- digichem/result/vibration.py +167 -0
- digichem/test/__init__.py +6 -0
- digichem/test/conftest.py +4 -0
- digichem/test/test_basis.py +71 -0
- digichem/test/test_calculate.py +30 -0
- digichem/test/test_config.py +78 -0
- digichem/test/test_cube.py +369 -0
- digichem/test/test_exception.py +16 -0
- digichem/test/test_file.py +104 -0
- digichem/test/test_image.py +337 -0
- digichem/test/test_input.py +64 -0
- digichem/test/test_parsing.py +79 -0
- digichem/test/test_prattle.py +36 -0
- digichem/test/test_result.py +489 -0
- digichem/test/test_translate.py +112 -0
- digichem/test/util.py +207 -0
- digichem/translate.py +591 -0
- digichem_core-6.0.0rc1.dist-info/METADATA +96 -0
- digichem_core-6.0.0rc1.dist-info/RECORD +111 -0
- digichem_core-6.0.0rc1.dist-info/WHEEL +4 -0
- digichem_core-6.0.0rc1.dist-info/licenses/COPYING.md +10 -0
- digichem_core-6.0.0rc1.dist-info/licenses/LICENSE +11 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# Classes for representing MOs.
|
|
2
|
+
|
|
3
|
+
# General imports.
|
|
4
|
+
from itertools import filterfalse, zip_longest, chain
|
|
5
|
+
import warnings
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
from digichem.exception import Result_unavailable_error
|
|
9
|
+
from digichem.result import Result_container
|
|
10
|
+
from digichem.result import Result_object
|
|
11
|
+
from digichem.result import Floatable_mixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Molecular_orbital_list(Result_container):
|
|
15
|
+
"""
|
|
16
|
+
Class for representing a group of MOs.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# A warning issued when attempting to merge non-equivalent orbital lists.
|
|
20
|
+
MERGE_WARNING = "Attempting to merge lists of orbitals that are not identical; non-equivalent orbitals will be ignored"
|
|
21
|
+
|
|
22
|
+
def __init__(self, *args, **kwargs):
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def HOMO_energy(self):
|
|
27
|
+
"""
|
|
28
|
+
Get the energy of the highest occupied orbital in this list.
|
|
29
|
+
|
|
30
|
+
Use get_orbital(HOMO_difference = 0) to retrieve the HOMO as an object.
|
|
31
|
+
|
|
32
|
+
:raises Result_unavailable_error: If the HOMO is not available.
|
|
33
|
+
:return The orbital energy (in eV).
|
|
34
|
+
"""
|
|
35
|
+
return self.get_orbital(HOMO_difference = 0).energy
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def LUMO_energy(self):
|
|
39
|
+
"""
|
|
40
|
+
Get the energy of the lowest unoccupied orbital in this list.
|
|
41
|
+
|
|
42
|
+
Use get_orbital(HOMO_difference = 1) to retrieve the LUMO as an object.
|
|
43
|
+
|
|
44
|
+
:raises Result_unavailable_error: If the LUMO is not available.
|
|
45
|
+
:return The orbital energy (in eV).
|
|
46
|
+
"""
|
|
47
|
+
return self.get_orbital(HOMO_difference = 1).energy
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def HOMO_LUMO_energy(self):
|
|
51
|
+
"""
|
|
52
|
+
Get ΔE HOMO-LUMO; the energy difference between the HOMO and LUMO.
|
|
53
|
+
|
|
54
|
+
Depending on how the orbitals are occupied, it might not make sense to calculate the HOMO-LUMO energy.
|
|
55
|
+
|
|
56
|
+
:raises Result_unavailable_error: If the HOMO or LUMO is not available.
|
|
57
|
+
:return: The HOMO-LUMO energy gap (in eV).
|
|
58
|
+
"""
|
|
59
|
+
# Get out orbitals.
|
|
60
|
+
HOMO = self.get_orbital(HOMO_difference = 0)
|
|
61
|
+
LUMO = self.get_orbital(HOMO_difference = 1)
|
|
62
|
+
# Return the difference.
|
|
63
|
+
return float(LUMO) - float(HOMO)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def occupied(self):
|
|
67
|
+
"""
|
|
68
|
+
Return a new Molecular_orbital_list containing only the occupied orbitals of this list.
|
|
69
|
+
"""
|
|
70
|
+
return type(self)([orbital for orbital in self if orbital.is_occupied])
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def virtual(self):
|
|
74
|
+
"""
|
|
75
|
+
Return a new Molecular_orbital_list containing only the virtual (unoccupied) orbitals of this list.
|
|
76
|
+
"""
|
|
77
|
+
return type(self)([orbital for orbital in self if not orbital.is_occupied])
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def spin_type(self):
|
|
81
|
+
"""
|
|
82
|
+
Get the spin type (alpha, beta etc.) of the orbitals in this list.
|
|
83
|
+
|
|
84
|
+
:raises Result_unavailable_error: If there are no orbitals in this list.
|
|
85
|
+
:return: The spin type, one of either 'alpha' or 'beta' for unrestricted calcs, 'none' for restricted calcs, or 'mixed' if multiple spin types are present in this list.
|
|
86
|
+
"""
|
|
87
|
+
# Unique spin types in our list.
|
|
88
|
+
spin_types = list(set([orbital.spin_type for orbital in self]))
|
|
89
|
+
|
|
90
|
+
if len(spin_types) == 1:
|
|
91
|
+
return spin_types[0]
|
|
92
|
+
elif len(spin_types) > 1:
|
|
93
|
+
return "mixed"
|
|
94
|
+
else:
|
|
95
|
+
raise Result_unavailable_error("Orbital Spin", "There are no orbitals")
|
|
96
|
+
|
|
97
|
+
def get_orbital(self, criteria = None, *, label = None, HOMO_difference = None, level = None):
|
|
98
|
+
"""
|
|
99
|
+
Retrieve an orbital based on some property.
|
|
100
|
+
|
|
101
|
+
Only one of the criteria should be specified.
|
|
102
|
+
|
|
103
|
+
:deprecated: Use find() instead.
|
|
104
|
+
:raises Result_unavailable_error: If the requested MO could not be found.
|
|
105
|
+
:param criteria: A string describing the orbital to get. The meaning of criteria is determined automatically based on its content. If criteria begins with '+' or '-' then it is used as a HOMO_difference. Otherwise, if criteria is a valid integer, then it is used as a level. Otherwise, criteria is assumed to be an orbital label.
|
|
106
|
+
:param label: The label of the orbital to get.
|
|
107
|
+
:param HOMO_difference: The distance from the HOMO of the orbital to get.
|
|
108
|
+
:param level: The level of the orbital to get.
|
|
109
|
+
:return: The Molecular_orbital object.
|
|
110
|
+
"""
|
|
111
|
+
warnings.warn("get_orbital is deprecated, use find() instead", DeprecationWarning)
|
|
112
|
+
return self.find(criteria, label = label, HOMO_difference = HOMO_difference, level = level)
|
|
113
|
+
|
|
114
|
+
def find(self, criteria = None, *, label = None, HOMO_difference = None, level = None):
|
|
115
|
+
"""
|
|
116
|
+
Retrieve an orbital based on some property.
|
|
117
|
+
|
|
118
|
+
Only one of the criteria should be specified.
|
|
119
|
+
|
|
120
|
+
:raises Result_unavailable_error: If the requested MO could not be found.
|
|
121
|
+
:param criteria: A string describing the orbital to get. The meaning of criteria is determined automatically based on its content. If criteria begins with '+' or '-' then it is used as a HOMO_difference. Otherwise, if criteria is a valid integer, then it is used as a level. Otherwise, criteria is assumed to be an orbital label.
|
|
122
|
+
:param label: The label of the orbital to get.
|
|
123
|
+
:param HOMO_difference: The distance from the HOMO of the orbital to get.
|
|
124
|
+
:param level: The level of the orbital to get.
|
|
125
|
+
:return: The Molecular_orbital object.
|
|
126
|
+
"""
|
|
127
|
+
# Use the search() method to do our work for us.
|
|
128
|
+
return self.search(criteria, label = label, HOMO_difference = HOMO_difference, level = level, allow_empty = False)[0]
|
|
129
|
+
|
|
130
|
+
def search(self, criteria = None, *, label = None, HOMO_difference = None, level = None, allow_empty = True):
|
|
131
|
+
"""
|
|
132
|
+
Attempt to retrieve a number of orbitals based on some property.
|
|
133
|
+
|
|
134
|
+
Only one of the criteria should be specified.
|
|
135
|
+
|
|
136
|
+
:param criteria: A string describing the orbitals to get. The meaning of criteria is determined automatically based on its content. If criteria begins with '+' or '-' then it is used as a HOMO_difference. Otherwise, if criteria is a valid integer, then it is used as a level. Otherwise, criteria is assumed to be an orbital label.
|
|
137
|
+
:param label: The label of the orbitals to get.
|
|
138
|
+
:param HOMO_difference: The distance from the HOMO of the orbitals to get.
|
|
139
|
+
:param level: The level of the orbitals to get.
|
|
140
|
+
:param allow_empty: If False and no matching orbitals can be found, raise a Result_unavailable_error.
|
|
141
|
+
:return: A (possibly empty) Molecular_orbital_list object.
|
|
142
|
+
"""
|
|
143
|
+
# If we've been given a generic criteria, decide what it is actually talking about.
|
|
144
|
+
if criteria is not None:
|
|
145
|
+
try:
|
|
146
|
+
# If our string start with a sign (+ or -) then it's a HOMO_difference.
|
|
147
|
+
if criteria[:1] == "+" or criteria[:1] == "-":
|
|
148
|
+
HOMO_difference = int(criteria)
|
|
149
|
+
# If its and integer (or a string that looks like one), then its a level.
|
|
150
|
+
elif criteria.isdigit() or isinstance(criteria, int):
|
|
151
|
+
level = int(criteria)
|
|
152
|
+
# Otherwise we assume it a label.
|
|
153
|
+
else:
|
|
154
|
+
label = criteria
|
|
155
|
+
|
|
156
|
+
except Exception:
|
|
157
|
+
# We couldn't parse criteria, get upset.
|
|
158
|
+
raise ValueError("Unable to understand given search criteria '{}'".format(criteria))
|
|
159
|
+
|
|
160
|
+
# Now get our filter func.
|
|
161
|
+
if label is not None:
|
|
162
|
+
filter_func = lambda mo: mo.label != label
|
|
163
|
+
elif HOMO_difference is not None:
|
|
164
|
+
filter_func = lambda mo: mo.HOMO_difference != HOMO_difference
|
|
165
|
+
elif level is not None:
|
|
166
|
+
filter_func = lambda mo: mo.level != level
|
|
167
|
+
else:
|
|
168
|
+
raise ValueError("Missing criteria to search by; specify one of 'criteria', 'label', 'HOMO_difference' or 'level'")
|
|
169
|
+
|
|
170
|
+
# Now search.
|
|
171
|
+
found = type(self)(filterfalse(filter_func, self))
|
|
172
|
+
|
|
173
|
+
# Possibly panic if the list is empty.
|
|
174
|
+
if not allow_empty and len(found) == 0:
|
|
175
|
+
# Work out what we searched for to give better error.
|
|
176
|
+
if label is not None:
|
|
177
|
+
criteria_string = "label = '{}'".format(label)
|
|
178
|
+
|
|
179
|
+
elif HOMO_difference is not None:
|
|
180
|
+
criteria_string = "HOMO difference = '{}'".format(HOMO_difference)
|
|
181
|
+
|
|
182
|
+
elif level is not None:
|
|
183
|
+
criteria_string = "index = '{}'".format(level)
|
|
184
|
+
|
|
185
|
+
raise Result_unavailable_error("Orbital", "could not find orbital with {}".format(criteria_string))
|
|
186
|
+
|
|
187
|
+
return found
|
|
188
|
+
|
|
189
|
+
def ordered(self):
|
|
190
|
+
"""
|
|
191
|
+
Return a copy of this list of MOs that is ordered in terms of energy and removes duplicate MOs.
|
|
192
|
+
"""
|
|
193
|
+
ordered_list = type(self)(set(self))
|
|
194
|
+
#ordered_list.sort(key = lambda mo: mo.level)
|
|
195
|
+
ordered_list.sort()
|
|
196
|
+
return ordered_list
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def from_parser(self, parser, cls = None):
|
|
200
|
+
"""
|
|
201
|
+
Construct a Molecular_orbital_list object from an output file parser.
|
|
202
|
+
|
|
203
|
+
:param parser: An output file parser.
|
|
204
|
+
:param cls: Optional class of objects to populate this list with, should inherit from Molecular_orbital. Defaults to Molecular_orbital if only one set of orbitals are available, or Alpha_orbital if both alpha and beta are available (in which case you should call Molecular_orbital_list.from_cclib() again with cls = Beta_orbital to get beta as well).
|
|
205
|
+
:returns: The new Molecular_orbital_list object. The list will be empty if no MO data is available.
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
# Set our default class if we've not been given one.
|
|
209
|
+
if cls is None:
|
|
210
|
+
# Check to see if we have only 'alpha' or beta as well.
|
|
211
|
+
if len(parser.data.moenergies) == 1:
|
|
212
|
+
cls = Molecular_orbital
|
|
213
|
+
else:
|
|
214
|
+
cls = Alpha_orbital
|
|
215
|
+
# Get our list.
|
|
216
|
+
return self(cls.list_from_parser(parser))
|
|
217
|
+
except AttributeError:
|
|
218
|
+
return self()
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def from_dump(self, data, result_set, options):
|
|
222
|
+
"""
|
|
223
|
+
Get an instance of this class from its dumped representation.
|
|
224
|
+
|
|
225
|
+
:param data: The data to parse.
|
|
226
|
+
:param result_set: The partially constructed result set which is being populated.
|
|
227
|
+
"""
|
|
228
|
+
# Decide which class to use.
|
|
229
|
+
if data['spin_type'] == "none":
|
|
230
|
+
cls = Molecular_orbital
|
|
231
|
+
elif data['spin_type'] == "alpha":
|
|
232
|
+
cls = Alpha_orbital
|
|
233
|
+
elif data['spin_type'] == "beta":
|
|
234
|
+
cls = Beta_orbital
|
|
235
|
+
elif data['spin_type'] is None and len(data['values']) == 0:
|
|
236
|
+
# No orbitals, no type defined.
|
|
237
|
+
return self()
|
|
238
|
+
|
|
239
|
+
return self(cls.list_from_dump(data['values'], result_set, options))
|
|
240
|
+
|
|
241
|
+
def dump(self, digichem_options):
|
|
242
|
+
"""
|
|
243
|
+
Get a representation of this result object in primitive format.
|
|
244
|
+
"""
|
|
245
|
+
dump_dict = {
|
|
246
|
+
"dE(HOMO-LUMO)": {
|
|
247
|
+
"value": self.HOMO_LUMO_energy if self.safe_get("HOMO_LUMO_energy") else None,
|
|
248
|
+
"units": "eV"
|
|
249
|
+
},
|
|
250
|
+
"num_occupied": len(self.occupied),
|
|
251
|
+
"num_virtual": len(self.virtual),
|
|
252
|
+
"values": super().dump(digichem_options),
|
|
253
|
+
"spin_type": self.safe_get("spin_type")
|
|
254
|
+
}
|
|
255
|
+
# # Add HOMO and LUMO
|
|
256
|
+
# for label in ["HOMO", "LUMO"]:
|
|
257
|
+
# try:
|
|
258
|
+
# orbital = self.find(label)
|
|
259
|
+
# dump_dict[label] = {
|
|
260
|
+
# "value": float(orbital.energy),
|
|
261
|
+
# "units": "eV"
|
|
262
|
+
# }
|
|
263
|
+
#
|
|
264
|
+
# except Result_unavailable_error:
|
|
265
|
+
# pass
|
|
266
|
+
|
|
267
|
+
return dump_dict
|
|
268
|
+
|
|
269
|
+
def find_common_level(self, *other_lists, HOMO_difference):
|
|
270
|
+
"""
|
|
271
|
+
Find either:
|
|
272
|
+
The orbital with the lowest level that has no less than the given negative HOMO_difference.
|
|
273
|
+
or:
|
|
274
|
+
The orbital with the highest level that has no more than the given positive HOMO_difference.
|
|
275
|
+
Across one or more orbital lists.
|
|
276
|
+
|
|
277
|
+
The method is useful for determining which orbitals to traverse between two limits from the HOMO-LUMO gap.
|
|
278
|
+
|
|
279
|
+
:raises Result_unavailable_error: If all of the given orbital_lists (including this one) are empty.
|
|
280
|
+
:param *other_lists: Optional lists to search. If none are given, then only this orbital_list is search.
|
|
281
|
+
:param HOMO_difference: The distance from the HOMO to search for. Negative values indicate HOMO-n, positive values indicate LUMO+(n-1). The LUMO should be at +1 by definition.
|
|
282
|
+
:return: The level (as an integer) of the matching orbital.
|
|
283
|
+
"""
|
|
284
|
+
# Our list of orbitals that match our criteria.
|
|
285
|
+
found_orbitals = []
|
|
286
|
+
|
|
287
|
+
# Our list of orbital_list objects to look through.
|
|
288
|
+
orbital_lists = list(other_lists)
|
|
289
|
+
orbital_lists.append(self)
|
|
290
|
+
|
|
291
|
+
# Cant think of a better way to do this...
|
|
292
|
+
if HOMO_difference <= 0:
|
|
293
|
+
search_func = lambda orbital: orbital.HOMO_difference < HOMO_difference
|
|
294
|
+
else:
|
|
295
|
+
search_func = lambda orbital: orbital.HOMO_difference > HOMO_difference
|
|
296
|
+
|
|
297
|
+
# Loop through each list and search.
|
|
298
|
+
for orbital_list in orbital_lists:
|
|
299
|
+
# Now search each list for orbitals that match our criteria.
|
|
300
|
+
matching_orbitals = list(filterfalse(search_func, orbital_list))
|
|
301
|
+
|
|
302
|
+
# We can just add all the orbitals we find because we'll only look at the lowest/highest anyway.
|
|
303
|
+
found_orbitals.extend(matching_orbitals)
|
|
304
|
+
|
|
305
|
+
# Get a list of orbital levels that match our criteria.
|
|
306
|
+
orbital_levels = [orbital.level for orbital in found_orbitals]
|
|
307
|
+
|
|
308
|
+
# Now either return the smallest or largest orbital level, depending on what we were asked for.
|
|
309
|
+
try:
|
|
310
|
+
if HOMO_difference <= 0:
|
|
311
|
+
# The lowest orbital below HOMO.
|
|
312
|
+
return min(orbital_levels)
|
|
313
|
+
else:
|
|
314
|
+
# The highest orbital above LUMO.
|
|
315
|
+
return max(orbital_levels)
|
|
316
|
+
except ValueError:
|
|
317
|
+
# Min/Max couldn't find anything, this should only happen if all orbital_lists are completely empty.
|
|
318
|
+
raise Result_unavailable_error("Common orbital level", "there are no orbitals")
|
|
319
|
+
|
|
320
|
+
def assign_total_level(self, other_list):
|
|
321
|
+
"""
|
|
322
|
+
Assign total levels to the orbitals in this list and another orbital list.
|
|
323
|
+
|
|
324
|
+
'Total levels' are the index +1 of each orbital out of both alpha and beta orbitals.
|
|
325
|
+
|
|
326
|
+
:param: other_list: If this list contains alpha orbitals, other_list should contain beta_orbitals (vice versa).
|
|
327
|
+
"""
|
|
328
|
+
for index, orbital in enumerate(sorted(chain(self, other_list), key = lambda orbital: orbital.energy)):
|
|
329
|
+
orbital.total_level = index +1
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def merge_orbitals(self, molecular_orbital_lists, beta_orbital_lists):
|
|
333
|
+
"""
|
|
334
|
+
"""
|
|
335
|
+
if len(molecular_orbital_lists) != len(beta_orbital_lists):
|
|
336
|
+
# Panic.
|
|
337
|
+
raise TypeError("molecular_orbital_lists and beta_orbital_lists must be of the same length")
|
|
338
|
+
|
|
339
|
+
MOs = molecular_orbital_lists[0]
|
|
340
|
+
betas = beta_orbital_lists[0]
|
|
341
|
+
|
|
342
|
+
# Check all other lists are the same.
|
|
343
|
+
for MO_list, beta_list in zip(molecular_orbital_lists[1:], beta_orbital_lists[1:]):
|
|
344
|
+
# If this list has atoms and our current doesn't, use this as our base list.
|
|
345
|
+
# We only do this if both lists are empty, so we don't have alpha and beta orbitals from two different results.
|
|
346
|
+
if len(MOs) == 0 and len(betas) == 0:
|
|
347
|
+
if len(MO_list) > 0:
|
|
348
|
+
MOs = MO_list
|
|
349
|
+
if len(beta_list) > 0:
|
|
350
|
+
betas = beta_list
|
|
351
|
+
|
|
352
|
+
else:
|
|
353
|
+
MOs.check_equivalent(MOs, MO_list)
|
|
354
|
+
betas.check_equivalent(betas, beta_list)
|
|
355
|
+
|
|
356
|
+
return (MOs, betas)
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def check_equivalent(self, MOs, other_MO_list):
|
|
360
|
+
"""
|
|
361
|
+
Check that two MO lists are equivalent, issuing merge warnings if not.
|
|
362
|
+
"""
|
|
363
|
+
for index, MO in enumerate(other_MO_list):
|
|
364
|
+
try:
|
|
365
|
+
other_MO = MOs[index]
|
|
366
|
+
if not self.are_items_equal(MO, other_MO):
|
|
367
|
+
warnings.warn(self.MERGE_WARNING)
|
|
368
|
+
except IndexError:
|
|
369
|
+
warnings.warn(self.MERGE_WARNING)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
def merge(self, *multiple_lists):
|
|
374
|
+
"""
|
|
375
|
+
Merge multiple lists of MOs into a single list.
|
|
376
|
+
"""
|
|
377
|
+
raise NotImplementedError("Molecular orbital lists do not implement the merge() method; use merge_orbitals() instead")
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
def are_items_equal(self, MO, other_MO):
|
|
381
|
+
"""
|
|
382
|
+
A method which determines whether two items are the same for the purposes of merging.
|
|
383
|
+
"""
|
|
384
|
+
return math.isclose(MO.energy, other_MO.energy) and MO.level == other_MO.level and MO.spin_type == other_MO.spin_type
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class Molecular_orbital(Result_object, Floatable_mixin):
|
|
388
|
+
"""
|
|
389
|
+
Class representing a molecular orbital.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
# True MOs don't have a spin.
|
|
393
|
+
spin_type = "none"
|
|
394
|
+
|
|
395
|
+
def __init__(self, level, HOMO_difference, energy, symmetry = None, symmetry_level = None):
|
|
396
|
+
"""
|
|
397
|
+
Constructor for MOs.
|
|
398
|
+
|
|
399
|
+
:param level: The 'level' of this MO (essentially an index), where the lowest MO has a level of 1, increasing by 1 for each higher orbital.
|
|
400
|
+
:param HOMO_difference: The distance of this MO from the HOMO. A negative value means this orbital is HOMO-n. A positive value means this orbital is HOMO+n (or LUMO+(n-1). A value of 0 means this orbital is the HOMO. A value of +1 means this orbital is the LUMO.
|
|
401
|
+
:param energy: The energy of this MO (in eV).
|
|
402
|
+
:param symmetry: The symmetry of this MO.
|
|
403
|
+
:param symmetry_level: The ordered (by energy) index of this orbital in terms of orbitals that share the same symmetry.
|
|
404
|
+
"""
|
|
405
|
+
super().__init__()
|
|
406
|
+
self.level = level
|
|
407
|
+
self.HOMO_difference = HOMO_difference
|
|
408
|
+
self.symmetry = symmetry
|
|
409
|
+
self.symmetry_level = symmetry_level
|
|
410
|
+
self.energy = energy
|
|
411
|
+
|
|
412
|
+
# This needs to be set outside of this constructor, because it relies on multiple lists of orbitals being completed.
|
|
413
|
+
# Total level is the index +1 of this orbital out of both alpha and beta orbitals (for restricted this will == level).
|
|
414
|
+
self.total_level = None
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def is_occupied(self):
|
|
418
|
+
"""
|
|
419
|
+
Determine whether this orbital is occupied.
|
|
420
|
+
"""
|
|
421
|
+
return self.HOMO_difference <= 0
|
|
422
|
+
|
|
423
|
+
def __float__(self):
|
|
424
|
+
return float(self.energy)
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def HOMO_level(self):
|
|
428
|
+
"""
|
|
429
|
+
The level of the HOMO in the collection of orbitals of which this orbital is a member.
|
|
430
|
+
"""
|
|
431
|
+
return self.level - self.HOMO_difference
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def LUMO_level(self):
|
|
435
|
+
"""
|
|
436
|
+
The level of the LUMO in the collection of orbitals of which this orbital is a member.
|
|
437
|
+
"""
|
|
438
|
+
return self.HOMO_level +1
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def label(self):
|
|
442
|
+
"""
|
|
443
|
+
A label describing this MO in terms of its proximity to the HOMO and LUMO.
|
|
444
|
+
|
|
445
|
+
:return: A string label, of the form either HOMO-n or LUMO+n.
|
|
446
|
+
"""
|
|
447
|
+
# The label we return depends on how close to the HOMO we are.
|
|
448
|
+
if self.level == self.HOMO_level:
|
|
449
|
+
# We are the HOMO.
|
|
450
|
+
label = "HOMO"
|
|
451
|
+
elif self.level < self.HOMO_level:
|
|
452
|
+
# We are below the HOMO (and presumably occupied).
|
|
453
|
+
label = "HOMO{}".format(self.level - self.HOMO_level)
|
|
454
|
+
elif self.level == self.LUMO_level:
|
|
455
|
+
# We are the LUMO.
|
|
456
|
+
label = "LUMO"
|
|
457
|
+
else:
|
|
458
|
+
# We are above the LUMO (and presumably unoccupied).
|
|
459
|
+
label = "LUMO{0:+}".format(self.level - self.LUMO_level)
|
|
460
|
+
|
|
461
|
+
return label
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def irrep(self):
|
|
465
|
+
"""
|
|
466
|
+
A unique description of this orbital combining symmetry and energy, as used by Turbomole.
|
|
467
|
+
"""
|
|
468
|
+
return "{}{}".format(self.symmetry_level, self.symmetry.lower()) if self.symmetry_level is not None and self.symmetry is not None else None
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def sirrep(self):
|
|
472
|
+
"""
|
|
473
|
+
A unique description of this orbital combining symmetry, energy and spin (alpha/beta), as used by Turbomole.
|
|
474
|
+
"""
|
|
475
|
+
if self.spin_type == "none":
|
|
476
|
+
return self.irrep
|
|
477
|
+
else:
|
|
478
|
+
if self.spin_type == "alpha":
|
|
479
|
+
spin_tag = "a"
|
|
480
|
+
elif self.spin_type == "beta":
|
|
481
|
+
spin_tag = "b"
|
|
482
|
+
else:
|
|
483
|
+
spin_tag = "?"
|
|
484
|
+
return "{}_{}".format(self.irrep, spin_tag)
|
|
485
|
+
|
|
486
|
+
def __eq__(self, other):
|
|
487
|
+
"""
|
|
488
|
+
Equality operator between MOs.
|
|
489
|
+
"""
|
|
490
|
+
return self.label == other.label
|
|
491
|
+
|
|
492
|
+
def __hash__(self):
|
|
493
|
+
"""
|
|
494
|
+
Hash operator.
|
|
495
|
+
"""
|
|
496
|
+
return hash(tuple(self.label))
|
|
497
|
+
|
|
498
|
+
# The index used to access data from cclib (which always has two lists, one for alpha one for beta).
|
|
499
|
+
ccdata_index = 0
|
|
500
|
+
|
|
501
|
+
def dump(self, digichem_options):
|
|
502
|
+
"""
|
|
503
|
+
Get a representation of this result object in primitive format.
|
|
504
|
+
"""
|
|
505
|
+
return {
|
|
506
|
+
"index": self.level,
|
|
507
|
+
"label": self.label,
|
|
508
|
+
"homo_distance": int(self.HOMO_difference),
|
|
509
|
+
"energy": {
|
|
510
|
+
"value": float(self.energy),
|
|
511
|
+
"units": "eV"
|
|
512
|
+
},
|
|
513
|
+
"symmetry": self.symmetry,
|
|
514
|
+
"symmetry_index": self.symmetry_level
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
@classmethod
|
|
518
|
+
def list_from_dump(self, data, result_set, options):
|
|
519
|
+
"""
|
|
520
|
+
Get a list of instances of this class from its dumped representation.
|
|
521
|
+
|
|
522
|
+
:param data: The data to parse.
|
|
523
|
+
:param result_set: The partially constructed result set which is being populated.
|
|
524
|
+
"""
|
|
525
|
+
return [self(orbital_dict['index'], orbital_dict['homo_distance'], orbital_dict['energy']['value'], orbital_dict['symmetry'], orbital_dict['symmetry_index']) for orbital_dict in data]
|
|
526
|
+
|
|
527
|
+
@classmethod
|
|
528
|
+
def list_from_parser(self, parser):
|
|
529
|
+
"""
|
|
530
|
+
Create a list of Molecular_orbital objects from an output file parser.
|
|
531
|
+
|
|
532
|
+
:param parser: An output file parser.
|
|
533
|
+
:return: A list of Molecular_orbital objects. The list will be empty if no MO is available.
|
|
534
|
+
"""
|
|
535
|
+
try:
|
|
536
|
+
# Get and zip required params.
|
|
537
|
+
if hasattr(parser.data, "mosyms"):
|
|
538
|
+
# We have symmetries.
|
|
539
|
+
symmetry = parser.data.mosyms[self.ccdata_index]
|
|
540
|
+
else:
|
|
541
|
+
# No symmetry.
|
|
542
|
+
symmetry = []
|
|
543
|
+
|
|
544
|
+
# Don't catch this exception; if we don't have MO energies there's nothing we can do.
|
|
545
|
+
energy = parser.data.moenergies[self.ccdata_index]
|
|
546
|
+
|
|
547
|
+
# Check we don't have more symmetries than we do energies.
|
|
548
|
+
if len(energy) < len(symmetry):
|
|
549
|
+
diff = len(symmetry) - len(energy)
|
|
550
|
+
warnings.warn("Parsed more MO symmetries than MO energies; excess symmetries will be discarded ('{}' symmetries, '{}' energies)".format(len(symmetry), len(energy)))
|
|
551
|
+
# Dump the excess symmetry.
|
|
552
|
+
symmetry = symmetry[:-diff]
|
|
553
|
+
|
|
554
|
+
# Keep a track of all the symmetries we've seen.
|
|
555
|
+
symmetries = {}
|
|
556
|
+
|
|
557
|
+
orbitals = []
|
|
558
|
+
for index, (symmetry, energy) in enumerate(zip_longest(symmetry, energy, fillvalue = None)):
|
|
559
|
+
if symmetry is not None:
|
|
560
|
+
try:
|
|
561
|
+
# Add one to the level.
|
|
562
|
+
symmetries[symmetry] += 1
|
|
563
|
+
symm_level = symmetries[symmetry]
|
|
564
|
+
except KeyError:
|
|
565
|
+
# We haven't seen this mult before.
|
|
566
|
+
symmetries[symmetry] = 1
|
|
567
|
+
symm_level = symmetries[symmetry]
|
|
568
|
+
else:
|
|
569
|
+
symm_level = None
|
|
570
|
+
|
|
571
|
+
orbitals.append(
|
|
572
|
+
self(
|
|
573
|
+
index +1,
|
|
574
|
+
index - parser.data.homos[self.ccdata_index],
|
|
575
|
+
energy,
|
|
576
|
+
symmetry,
|
|
577
|
+
symm_level
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
return orbitals
|
|
582
|
+
|
|
583
|
+
except (AttributeError, IndexError):
|
|
584
|
+
return []
|
|
585
|
+
|
|
586
|
+
class Unrestricted_orbital(Molecular_orbital):
|
|
587
|
+
"""
|
|
588
|
+
Top-level class for unrestricted orbitals.
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
def __init__(self, level, HOMO_difference, energy, spin_type, symmetry = None, symm_level = None):
|
|
592
|
+
"""
|
|
593
|
+
Constructor for MOs.
|
|
594
|
+
|
|
595
|
+
:param level: The 'level' of the MO (essentially an index), where the lowest MO has a level of 1, increasing by 1 for each higher orbital.
|
|
596
|
+
:param HOMO_difference: The distance of this MO from the HOMO. A negative value means this orbital is HOMO-n. A positive value means this orbital is HOMO+n (or LUMO+(n-1). A value of 0 means this orbital is the HOMO. A value of +1 means this orbital is the LUMO.
|
|
597
|
+
:param symmetry: The symmetry of the MO.
|
|
598
|
+
:param energy: The energy of the MO (in eV).
|
|
599
|
+
:param spin_type: The spin of this spin-orbital (either alpha or beta).
|
|
600
|
+
"""
|
|
601
|
+
# Call parent first.
|
|
602
|
+
super().__init__(level, HOMO_difference, energy, symmetry, symm_level)
|
|
603
|
+
self.spin_type = spin_type
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def label(self):
|
|
607
|
+
# Get the base of the label first.
|
|
608
|
+
label = super().label
|
|
609
|
+
# Append our spin type.
|
|
610
|
+
label = "{} ({})".format(label, self.spin_type)
|
|
611
|
+
return label
|
|
612
|
+
|
|
613
|
+
class Alpha_orbital(Unrestricted_orbital):
|
|
614
|
+
"""
|
|
615
|
+
An alpha spin orbital (these types of orbitals are only singly occupied, electrons are spin-up).
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
def __init__(self, level, HOMO_difference, energy, symmetry, symm_level):
|
|
619
|
+
"""
|
|
620
|
+
Constructor for alpha MOs.
|
|
621
|
+
|
|
622
|
+
:param level: The 'level' of the MO (essentially an index), where the lowest MO has a level of 1, increasing by 1 for each higher orbital.
|
|
623
|
+
:param HOMO_difference: The distance of this MO from the HOMO. A negative value means this orbital is HOMO-n. A positive value means this orbital is HOMO+n (or LUMO+(n-1). A value of 0 means this orbital is the HOMO. A value of +1 means this orbital is the LUMO.
|
|
624
|
+
:param energy: The energy of the MO (in eV).
|
|
625
|
+
:param symmetry: The symmetry of the MO.
|
|
626
|
+
"""
|
|
627
|
+
super().__init__(level, HOMO_difference, energy, "alpha", symmetry, symm_level)
|
|
628
|
+
|
|
629
|
+
class Beta_orbital(Unrestricted_orbital):
|
|
630
|
+
"""
|
|
631
|
+
A beta spin orbital (these types of orbitals are only singly occupied, electrons are spin-down).
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
# Beta orbitals use the other list in cclib.
|
|
635
|
+
ccdata_index = 1
|
|
636
|
+
|
|
637
|
+
def __init__(self, level, HOMO_difference, energy, symmetry, symm_level):
|
|
638
|
+
"""
|
|
639
|
+
Constructor for beta MOs.
|
|
640
|
+
|
|
641
|
+
:param level: The 'level' of the MO (essentially an index), where the lowest MO has a level of 1, increasing by 1 for each higher orbital.
|
|
642
|
+
:param HOMO_difference: The distance of this MO from the HOMO. A negative value means this orbital is HOMO-n. A positive value means this orbital is HOMO+n (or LUMO+(n-1). A value of 0 means this orbital is the HOMO. A value of +1 means this orbital is the LUMO.
|
|
643
|
+
:param energy: The energy of the MO (in eV).
|
|
644
|
+
:param symmetry: The symmetry of the MO.
|
|
645
|
+
"""
|
|
646
|
+
super().__init__(level, HOMO_difference, energy, "beta", symmetry, symm_level)
|
|
647
|
+
|