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.
Files changed (111) hide show
  1. digichem/__init__.py +75 -0
  2. digichem/basis.py +116 -0
  3. digichem/config/README +3 -0
  4. digichem/config/__init__.py +5 -0
  5. digichem/config/base.py +321 -0
  6. digichem/config/locations.py +14 -0
  7. digichem/config/parse.py +90 -0
  8. digichem/config/util.py +117 -0
  9. digichem/data/README +4 -0
  10. digichem/data/batoms/COPYING +18 -0
  11. digichem/data/batoms/LICENSE +674 -0
  12. digichem/data/batoms/README +2 -0
  13. digichem/data/batoms/__init__.py +0 -0
  14. digichem/data/batoms/batoms-renderer.py +351 -0
  15. digichem/data/config/digichem.yaml +714 -0
  16. digichem/data/functionals.csv +15 -0
  17. digichem/data/solvents.csv +185 -0
  18. digichem/data/tachyon/COPYING.md +5 -0
  19. digichem/data/tachyon/LICENSE +30 -0
  20. digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
  21. digichem/data/vmd/common.tcl +468 -0
  22. digichem/data/vmd/generate_combined_orbital_images.tcl +70 -0
  23. digichem/data/vmd/generate_density_images.tcl +45 -0
  24. digichem/data/vmd/generate_dipole_images.tcl +68 -0
  25. digichem/data/vmd/generate_orbital_images.tcl +57 -0
  26. digichem/data/vmd/generate_spin_images.tcl +66 -0
  27. digichem/data/vmd/generate_structure_images.tcl +40 -0
  28. digichem/datas.py +14 -0
  29. digichem/exception/__init__.py +7 -0
  30. digichem/exception/base.py +133 -0
  31. digichem/exception/uncatchable.py +63 -0
  32. digichem/file/__init__.py +1 -0
  33. digichem/file/base.py +364 -0
  34. digichem/file/cube.py +284 -0
  35. digichem/file/fchk.py +94 -0
  36. digichem/file/prattle.py +277 -0
  37. digichem/file/types.py +97 -0
  38. digichem/image/__init__.py +6 -0
  39. digichem/image/base.py +113 -0
  40. digichem/image/excited_states.py +335 -0
  41. digichem/image/graph.py +293 -0
  42. digichem/image/orbitals.py +239 -0
  43. digichem/image/render.py +617 -0
  44. digichem/image/spectroscopy.py +797 -0
  45. digichem/image/structure.py +115 -0
  46. digichem/image/vmd.py +826 -0
  47. digichem/input/__init__.py +3 -0
  48. digichem/input/base.py +78 -0
  49. digichem/input/digichem_input.py +500 -0
  50. digichem/input/gaussian.py +140 -0
  51. digichem/log.py +179 -0
  52. digichem/memory.py +166 -0
  53. digichem/misc/__init__.py +4 -0
  54. digichem/misc/argparse.py +44 -0
  55. digichem/misc/base.py +61 -0
  56. digichem/misc/io.py +239 -0
  57. digichem/misc/layered_dict.py +285 -0
  58. digichem/misc/text.py +139 -0
  59. digichem/misc/time.py +73 -0
  60. digichem/parse/__init__.py +13 -0
  61. digichem/parse/base.py +220 -0
  62. digichem/parse/cclib.py +138 -0
  63. digichem/parse/dump.py +253 -0
  64. digichem/parse/gaussian.py +130 -0
  65. digichem/parse/orca.py +96 -0
  66. digichem/parse/turbomole.py +201 -0
  67. digichem/parse/util.py +523 -0
  68. digichem/result/__init__.py +6 -0
  69. digichem/result/alignment/AA.py +114 -0
  70. digichem/result/alignment/AAA.py +61 -0
  71. digichem/result/alignment/FAP.py +148 -0
  72. digichem/result/alignment/__init__.py +3 -0
  73. digichem/result/alignment/base.py +310 -0
  74. digichem/result/angle.py +153 -0
  75. digichem/result/atom.py +742 -0
  76. digichem/result/base.py +258 -0
  77. digichem/result/dipole_moment.py +332 -0
  78. digichem/result/emission.py +402 -0
  79. digichem/result/energy.py +323 -0
  80. digichem/result/excited_state.py +821 -0
  81. digichem/result/ground_state.py +94 -0
  82. digichem/result/metadata.py +644 -0
  83. digichem/result/multi.py +98 -0
  84. digichem/result/nmr.py +1086 -0
  85. digichem/result/orbital.py +647 -0
  86. digichem/result/result.py +244 -0
  87. digichem/result/soc.py +272 -0
  88. digichem/result/spectroscopy.py +514 -0
  89. digichem/result/tdm.py +267 -0
  90. digichem/result/vibration.py +167 -0
  91. digichem/test/__init__.py +6 -0
  92. digichem/test/conftest.py +4 -0
  93. digichem/test/test_basis.py +71 -0
  94. digichem/test/test_calculate.py +30 -0
  95. digichem/test/test_config.py +78 -0
  96. digichem/test/test_cube.py +369 -0
  97. digichem/test/test_exception.py +16 -0
  98. digichem/test/test_file.py +104 -0
  99. digichem/test/test_image.py +337 -0
  100. digichem/test/test_input.py +64 -0
  101. digichem/test/test_parsing.py +79 -0
  102. digichem/test/test_prattle.py +36 -0
  103. digichem/test/test_result.py +489 -0
  104. digichem/test/test_translate.py +112 -0
  105. digichem/test/util.py +207 -0
  106. digichem/translate.py +591 -0
  107. digichem_core-6.0.0rc1.dist-info/METADATA +96 -0
  108. digichem_core-6.0.0rc1.dist-info/RECORD +111 -0
  109. digichem_core-6.0.0rc1.dist-info/WHEEL +4 -0
  110. digichem_core-6.0.0rc1.dist-info/licenses/COPYING.md +10 -0
  111. 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
+