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
digichem/result/tdm.py ADDED
@@ -0,0 +1,267 @@
1
+ import itertools
2
+
3
+ from digichem.exception.base import Result_unavailable_error
4
+ from digichem.result.dipole_moment import Electric_dipole_moment_mixin, Dipole_moment_ABC, Magnetic_dipole_moment_mixin
5
+ from digichem.result.base import Result_object
6
+
7
+
8
+ class Transition_dipole_moment_ABC(Dipole_moment_ABC):
9
+ """
10
+ ABC for electric and magnetic transition dipole moments.
11
+ """
12
+
13
+ def __init__(self, state_level, *args, **kwargs):
14
+ """
15
+ Electric_transition_dipole_moment constructor.
16
+
17
+ :param state: The excited state level this dipole transitions to.
18
+ """
19
+ super().__init__(*args, **kwargs)
20
+
21
+ # An int describing the excited state we belong to. This is always available.
22
+ self.state_level = state_level
23
+
24
+ # The excited state object that we belong to. This may be None. We can't set this here because then both the Excited_state and Transition_dipole_moment constructors would depend on each other.
25
+ self.excited_state = None
26
+
27
+ # Save a name describing which dipole we are (permanent vs transition etc).
28
+ self.dipole_type = "transition"
29
+
30
+ @classmethod
31
+ def from_dump(self, data, result_set, options):
32
+ """
33
+ Get a list of instances of this class from its dumped representation.
34
+
35
+ :param data: The data to parse.
36
+ :param result_set: The partially constructed result set which is being populated.
37
+ """
38
+ return self(
39
+ data['state_level'],
40
+ (data['origin']['x']['value'], data['origin']['y']['value'], data['origin']['z']['value']),
41
+ (data['vector']['x']['value'], data['vector']['y']['value'], data['vector']['z']['value']),
42
+ atoms = result_set.atoms
43
+ )
44
+
45
+ def dump(self, digichem_options):
46
+ """
47
+ Get a representation of this result object in primitive format.
48
+ """
49
+ data = {'state_level': self.state_level}
50
+ data.update(super().dump(digichem_options))
51
+ return data
52
+
53
+ @property
54
+ def name(self):
55
+ """
56
+ Name that describes this dipole moment.
57
+ """
58
+ return "{} TDM".format(self.excited_state.state_symbol)
59
+
60
+ def set_excited_state(self, excited_state):
61
+ """
62
+ Set the excited state object that we belong to.
63
+ """
64
+ # Not sure what we should do if the given excited_state has a different level to what we expect.
65
+ if excited_state.level != self.state_level:
66
+ pass
67
+ self.excited_state = excited_state
68
+
69
+
70
+ class Electric_transition_dipole_moment(Transition_dipole_moment_ABC, Electric_dipole_moment_mixin):
71
+ """
72
+ Class that represents an electric transition dipole moment.
73
+ """
74
+
75
+ def __init__(self, *args, **kwargs):
76
+ super().__init__(*args, **kwargs)
77
+ self.electromagnetic_type = "electric"
78
+
79
+ @classmethod
80
+ def list_from_parser(self, parser):
81
+ """
82
+ Get a list of TDMs from an output file parser.
83
+
84
+ :param parser: An output file parser.
85
+ :return: A list of TDM objects. An empty list will be returned if no TDM data is available.
86
+ """
87
+ try:
88
+ return [
89
+ self(state_level = index +1,
90
+ origin_coords = (0,0,0),
91
+ vector_coords = (parser.au_to_debye(tdm[0]), parser.au_to_debye(tdm[1]), parser.au_to_debye(tdm[2])),
92
+ atoms = parser.results.atoms) for index, tdm in enumerate(parser.data.etdips)]
93
+ except AttributeError:
94
+ return []
95
+
96
+
97
+ class Magnetic_transition_dipole_moment(Transition_dipole_moment_ABC, Magnetic_dipole_moment_mixin):
98
+ """
99
+ Class that represents a magnetic transition dipole moment.
100
+
101
+ Unlike electric dipole moments, the units here are in a.u. (0.5 bohr magnetons).
102
+ """
103
+
104
+ def __init__(self, *args, **kwargs):
105
+ super().__init__(*args, **kwargs)
106
+ self.electromagnetic_type = "magnetic"
107
+
108
+ @classmethod
109
+ def list_from_parser(self, parser):
110
+ """
111
+ Get a list of TDMs from an output file parser.
112
+
113
+ :param parser: An output file parser.
114
+ :return: A list of TDM objects. An empty list will be returned if no TDM data is available.
115
+ """
116
+ try:
117
+ return [
118
+ self(state_level = index +1,
119
+ origin_coords = (0,0,0),
120
+ vector_coords = tdm,
121
+ atoms = parser.results.atoms) for index, tdm in enumerate(parser.data.etmagdips)]
122
+ except AttributeError:
123
+ return []
124
+
125
+
126
+ class Transition_dipole_moment(Result_object):
127
+ """
128
+ A compound class that represents both the electric and magnetic components of a transition dipole moment.
129
+
130
+ This class can also be used in most places where an electric TDM is expected, as it will pass references to TEDM attributes to the actual TEDM class.
131
+ If the object has only a TMDM and no TEDM, references will instead be passed to the TMDM.
132
+ """
133
+
134
+ def __init__(self, electric = None, magnetic = None):
135
+ """
136
+ Constructor for TDM.
137
+
138
+ Either or both of the substituent TDM can be None.
139
+ """
140
+ self.electric = electric
141
+ self.magnetic = magnetic
142
+ self.electromagnetic_type = "electromagnetic"
143
+
144
+ def set_excited_state(self, excited_state):
145
+ """
146
+ Set the excited state object that we belong to.
147
+ """
148
+ if self.electric is not None:
149
+ self.electric.set_excited_state(excited_state)
150
+
151
+ if self.magnetic is not None:
152
+ self.magnetic.set_excited_state(excited_state)
153
+
154
+ @classmethod
155
+ def from_parser(self, parser):
156
+ return self.list_from_parser(parser)
157
+
158
+ @classmethod
159
+ def list_from_parser(self, parser):
160
+ """
161
+ Get a list of Transition_dipole_moment from an output file parser.
162
+
163
+ :param parser: An output file parser.
164
+ :return: A list of Transition_dipole_moment objects. An empty list will be returned if no TDM data is available.
165
+ """
166
+ electric = Electric_transition_dipole_moment.list_from_parser(parser)
167
+ magnetic = Magnetic_transition_dipole_moment.list_from_parser(parser)
168
+
169
+ return [self(electric_part, magnetic_part) for electric_part, magnetic_part in itertools.zip_longest(electric, magnetic)]
170
+
171
+ def __getattr__(self, name):
172
+ # This check prevents infinite recursion when pickling, and is probably a sensible check anyway...
173
+ if name != "electric" and name != "magnetic":
174
+ try:
175
+ if self.electric is not None:
176
+ return getattr(self.electric, name)
177
+ else:
178
+ return getattr(self.magnetic, name)
179
+ except AttributeError as e:
180
+ raise AttributeError(name) from e
181
+ else:
182
+ raise AttributeError(name)
183
+
184
+ @classmethod
185
+ def list_from_dump(self, data, result_set, options):
186
+ """
187
+ Get a list of instances of this class from its dumped representation.
188
+
189
+ :param data: The data to parse.
190
+ :param result_set: The partially constructed result set which is being populated.
191
+ """
192
+ # This constructor for tdms is a bit odd in that it does not work anything like the other from_dump() methods.
193
+ # In the dumped format, tdms are not stored as their own list (but rather as a sub dict of the relevant excited state),
194
+ # but in order to avoid recursive imports, a list of tdms does need to be created when excited states are loaded again.
195
+ # This being the case, 'data' here is actually a list of excited states, not tdms...
196
+ return [self.from_dump(excited_state['tdm'], result_set, options) for excited_state in data]
197
+
198
+ @classmethod
199
+ def from_dump(self, data, result_set, options):
200
+ """
201
+ Get a list of instances of this class from its dumped representation.
202
+
203
+ :param data: The data to parse.
204
+ :param result_set: The partially constructed result set which is being populated.
205
+ """
206
+ if data is None:
207
+ return None
208
+
209
+ # Build the individual parts.
210
+ if data['electric'] is not None:
211
+ electric = Electric_transition_dipole_moment.from_dump(data['electric'], result_set, options)
212
+
213
+ else:
214
+ electric = None
215
+
216
+ if data['magnetic'] is not None:
217
+ magnetic = Magnetic_transition_dipole_moment.from_dump(data['magnetic'], result_set, options)
218
+
219
+ else:
220
+ magnetic = None
221
+
222
+ return self(electric = electric, magnetic = magnetic)
223
+
224
+ def dump(self, digichem_options):
225
+ """
226
+ Get a representation of this result object in primitive format.
227
+ """
228
+ return {
229
+ "electric": self.electric.dump(digichem_options) if self.electric is not None else None,
230
+ "magnetic": self.magnetic.dump(digichem_options) if self.magnetic is not None else None,
231
+ "angle": {
232
+ "value": float(self.angle().angle) if self.electric is not None and self.magnetic is not None else None,
233
+ "units": self.angle().units if self.electric is not None and self.magnetic is not None else None,
234
+ },
235
+ "cos_angle": float(self.cos_angle()) if self.electric is not None and self.magnetic is not None else None,
236
+ "dissymmetry_factor": float(self.g_value) if self.safe_get('g_value') is not None else None
237
+ }
238
+
239
+
240
+ def angle(self, cgs = True):
241
+ """
242
+ Return the angle between the electric and magnetic components of this transition dipole moment.
243
+ """
244
+ return self.electric.angle(self.magnetic, cgs)
245
+
246
+ def cos_angle(self, cgs = True):
247
+ """
248
+ Return the cos of the angle between the electric and magnetic components of this transition dipole moment.
249
+ """
250
+ return self.electric.cos_angle(self.magnetic, cgs)
251
+
252
+ @property
253
+ def g_value(self):
254
+ """
255
+ Return the 'g value'; the dissymmerty factor of this transition dipole moment.
256
+
257
+ This calculation is based on J. Phys. Chem. Lett. 2021, 12, 1, 686–695.
258
+ """
259
+ try:
260
+ return (4 * self.electric.gaussian_cgs * self.magnetic.gaussian_cgs * self.cos_angle(True)) / (self.electric.gaussian_cgs **2 + self.magnetic.gaussian_cgs **2)
261
+
262
+ except (FloatingPointError, ZeroDivisionError):
263
+ return 0
264
+
265
+ except AttributeError:
266
+ raise Result_unavailable_error("g_value", "transition electric or transition magnetic dipole moment is not available") from None
267
+
@@ -0,0 +1,167 @@
1
+ # General imports.
2
+ from itertools import zip_longest
3
+ import warnings
4
+
5
+ # Digichem imports.
6
+ from digichem.result import Result_container
7
+ from digichem.result import Result_object
8
+ from digichem.result import Floatable_mixin, Unmergeable_container_mixin
9
+
10
+ # Hidden.
11
+ #from digichem.result.spectroscopy import Spectroscopy_graph
12
+
13
+
14
+ class Vibrations_list(Result_container, Unmergeable_container_mixin):
15
+ """
16
+ Class for representing a group of molecular vibrations.
17
+ """
18
+
19
+ MERGE_WARNING = "Attempting to merge lists of vibrations that are not identical; non-equivalent vibrations will be ignored"
20
+
21
+ @property
22
+ def negative_frequencies(self):
23
+ """
24
+ Get a Vibrations_list object of the vibrations in this list that have negative frequencies (they are imaginary).
25
+ """
26
+ warnings.warn("negative_frequencies is deprecated, use negative instead")
27
+ return self.negative
28
+
29
+ @property
30
+ def negative(self):
31
+ """
32
+ Get a Vibrations_list object of the vibrations in this list that have negative frequencies (they are imaginary).
33
+ """
34
+ return type(self)([vibration for vibration in self if vibration.frequency < 0])
35
+
36
+ @classmethod
37
+ def from_parser(self, parser):
38
+ """
39
+ Get an Vibrations_list object from an output file parser.
40
+
41
+ :param parser: An output file parser.
42
+ :return: A Vibrations_list object. The list will be empty if no vibration frequency data is available.
43
+ """
44
+ return self(Vibration.list_from_parser(parser))
45
+
46
+ def generate_for_dump(self):
47
+ """
48
+ Method used to get a dictionary used to generate on-demand values for dumping.
49
+
50
+ This functionality is useful for hiding expense properties from the normal dump process, while still exposing them when specifically requested.
51
+
52
+ Each key in the returned dict is the name of a dumpable item, each value is a function to call with digichem_options as its only param.
53
+ """
54
+ return {"spectrum": self.generate_spectrum}
55
+
56
+ def generate_spectrum(self, digichem_options):
57
+ """
58
+ Abstract function that is called to generate an on-demand value for dumping.
59
+
60
+ This functionality is useful for hiding expense properties from the normal dump process, while still exposing them when specifically requested.
61
+
62
+ :param key: The key being requested.
63
+ :param digichem_options: Digichem options being used to dump.
64
+ """
65
+ from digichem.result.spectroscopy import Spectroscopy_graph
66
+
67
+
68
+ spectrum = Spectroscopy_graph.from_vibrations(self, digichem_options['IR_spectrum']['fwhm'], digichem_options['IR_spectrum']['gaussian_resolution'], digichem_options['IR_spectrum']['gaussian_cutoff'])
69
+
70
+ try:
71
+ spectrum_data = spectrum.plot_cumulative_gaussian()
72
+
73
+ spectrum_data = [{"x":{"value": float(x), "units": "c m^-1"}, "y": {"value":float(y), "units": "km mol^-1"}} for x,y in spectrum_data]
74
+ spectrum_peaks = [{"x":{"value": float(x), "units": "c m^-1"}, "y": {"value":float(y), "units": "km mol^-1"}} for x, y in spectrum.peaks()]
75
+
76
+ except Exception:
77
+ spectrum_data = []
78
+ spectrum_peaks = []
79
+
80
+ return {
81
+ "values": spectrum_data,
82
+ "peaks": spectrum_peaks
83
+ }
84
+
85
+ def dump(self, digichem_options):
86
+ dump_dict = {
87
+ "num_vibrations": len(self),
88
+ "num_negative": len(self.negative),
89
+ "values": super().dump(digichem_options),
90
+ }
91
+ return dump_dict
92
+
93
+ @classmethod
94
+ def from_dump(self, data, result_set, options):
95
+ """
96
+ Get an instance of this class from its dumped representation.
97
+
98
+ :param data: The data to parse.
99
+ :param result_set: The partially constructed result set which is being populated.
100
+ """
101
+ return self(Vibration.list_from_dump(data['values'], result_set, options))
102
+
103
+
104
+ class Vibration(Result_object, Floatable_mixin):
105
+ """
106
+ Class for representing vibrational frequencies.
107
+ """
108
+
109
+ def __init__(self, level, frequency, intensity, symmetry = None):
110
+ """
111
+ Vibration object constructor.
112
+
113
+ :param level: The numerical index of the vibration.
114
+ :param frequency: Frequency of the vibration (cm-1).
115
+ :param intensity: Intensity of the vibration in IR (km mol-1)
116
+ :param symmetry: Symmetry term of the vibration (string).
117
+ """
118
+ self.level = level
119
+ self.symmetry = symmetry
120
+ self.frequency = frequency
121
+ self.intensity = intensity
122
+
123
+ def __float__(self):
124
+ """
125
+ A float representation of this object.
126
+ """
127
+ return float(self.frequency)
128
+
129
+ def dump(self, digichem_options):
130
+ """
131
+ Get a representation of this result object in primitive format.
132
+ """
133
+ return {
134
+ "index": self.level,
135
+ "frequency": {
136
+ "value": float(self.frequency),
137
+ "units": "c m^-1",
138
+ },
139
+ "intensity": {
140
+ "value": float(self.intensity),
141
+ "units": "km mol^-1"
142
+ },
143
+ "symmetry": self.symmetry
144
+ }
145
+
146
+ @classmethod
147
+ def list_from_dump(self, data, result_set, options):
148
+ """
149
+ Get a list of instances of this class from its dumped representation.
150
+
151
+ :param data: The data to parse.
152
+ :param result_set: The partially constructed result set which is being populated.
153
+ """
154
+ return [self(vib_dict['index'], vib_dict['frequency']['value'], vib_dict['intensity']['value'], vib_dict['symmetry']) for vib_dict in data]
155
+
156
+ @classmethod
157
+ def list_from_parser(self, parser):
158
+ """
159
+ Create a list of Vibration objects from an output file parser.
160
+
161
+ :param parser: An output file parser.
162
+ :return: A list of Vibration objects. The list will be empty if no frequency data is available.
163
+ """
164
+ try:
165
+ return [self(index+1, frequency, intensity, symmetry) for index, (symmetry, frequency, intensity) in enumerate(zip_longest(getattr(parser.data, 'vibsyms', []), parser.data.vibfreqs, parser.data.vibirs, fillvalue = None))]
166
+ except AttributeError:
167
+ return []
@@ -0,0 +1,6 @@
1
+ """Unit tests for digichem"""
2
+
3
+ from pathlib import Path
4
+
5
+ import digichem.log
6
+ digichem.log.set_logging_level("DEBUG")
@@ -0,0 +1,4 @@
1
+ import numpy
2
+
3
+ # Set numpy errors (not sure why this isn't the default...)
4
+ numpy.seterr(invalid = 'raise', divide = 'raise')
@@ -0,0 +1,71 @@
1
+ """Tests for handling basis sets from BSE."""
2
+ import pytest
3
+
4
+ from digichem.basis import BSE_basis_set
5
+
6
+ def test_basis():
7
+ """Can we load a simple basis set?"""
8
+ basis = BSE_basis_set({
9
+ "STO-3G":"all"
10
+ })
11
+ basis.to_format()
12
+
13
+ def test_ecps():
14
+ """Can we load a simple basis set eith ecps?"""
15
+ basis = BSE_basis_set({
16
+ "LANL2DZ":"all"
17
+ })
18
+ assert basis.has_ECPs()
19
+
20
+ @pytest.mark.parametrize("set", [
21
+ {"STO-3G":"all"},
22
+ {"STO-3G":"C"},
23
+ {"STO-3G":"C,H"},
24
+ {"STO-3G":"H","def2-SVP":"C"}
25
+ ], ids = ["all", "C", "C,H", "mixed"])
26
+ def test_meta(set):
27
+ """Can we handle basis set metadata correctly?"""
28
+ basis = BSE_basis_set(set)
29
+ str(basis)
30
+
31
+ @pytest.mark.parametrize("filter, number", [
32
+ ("C", 1),
33
+ ("C,H", 2),
34
+ (("C", "H"), 2)
35
+ ], ids = ["C", "C,H", list])
36
+ def test_filter(filter, number):
37
+ """Can we return only parts of a basis set?"""
38
+ basis = BSE_basis_set({
39
+ "STO-3G":"all"
40
+ })
41
+ output = basis.to_format("gaussian94", filter)
42
+ assert output.count("****") == number
43
+
44
+ def test_mixed_basis():
45
+ """Can we combine multiple basis sets?"""
46
+ basis = BSE_basis_set({
47
+ "STO-3G":"H",
48
+ "def2-SVP" : "C"
49
+ })
50
+
51
+ basis.to_format("gaussian94")
52
+
53
+ def test_mixed_basis_filter():
54
+ """Can we combine multiple basis sets?"""
55
+ basis = BSE_basis_set({
56
+ "STO-3G":"H",
57
+ "def2-SVP" : "C"
58
+ })
59
+
60
+ output = basis.to_format("gaussian94", "C,H")
61
+ assert output.count("****") == 2
62
+
63
+ def test_wrong_basis():
64
+ """Check a non-existent basis set is flagged"""
65
+ basis = BSE_basis_set({
66
+ "STO-3G":"H",
67
+ "defs2-SVP" : "C"
68
+ })
69
+
70
+ with pytest.raises(KeyError):
71
+ basis.to_format("gaussian94")
@@ -0,0 +1,30 @@
1
+ """Tests for calculated results"""
2
+
3
+ import pytest
4
+ from pathlib import Path
5
+ import shutil
6
+
7
+ from digichem.test.util import data_directory, digichem_options
8
+ from digichem.parse.util import parse_calculation
9
+
10
+
11
+ @pytest.fixture(scope="module")
12
+ def formaldehyde_SOC_result(digichem_options):
13
+ return parse_calculation(Path(data_directory(), "Formaldehyde/Gaussian 09 Excited States TD-DFT 4 Singlets 4 Triplets B3LYP Gas Phase TZVP.tar.gz"), options = digichem_options)
14
+
15
+ @pytest.mark.skipif(not shutil.which("rwfdump"), reason="No rwfdump available")
16
+ @pytest.mark.parametrize("result_set, singlet, triplet, SOC", [
17
+ # These SOC values are taken from the original paper J. Chem. Theory Comput. 2017, 13, 515−524 for PySOC,
18
+ # the program Silioc uses to calculate SOC. We are limited in terms of accuracy by the number of decimals
19
+ # (0) that they report...
20
+ (pytest.lazy_fixture("formaldehyde_SOC_result"), "S(1)", "T(1)", 0.0),
21
+ (pytest.lazy_fixture("formaldehyde_SOC_result"), "S(1)", "T(2)", 45.0)
22
+ ])
23
+ def test_soc(result_set, singlet, triplet, SOC):
24
+ """Test calculation of spin-orbit coupling values."""
25
+
26
+ # Check we have SOC between each singlet state (including the ground) and each triplet state.
27
+ assert len(result_set.soc) == (result_set.excited_states.num_singlets +1) * result_set.excited_states.num_triplets
28
+
29
+ # Check some SOC values.
30
+ assert result_set.soc.between(singlet, triplet).wavenumbers == pytest.approx(SOC, abs=1e-0)
@@ -0,0 +1,78 @@
1
+ import pytest
2
+ from pathlib import Path
3
+ import yaml
4
+ import copy
5
+
6
+ from digichem.test.util import digichem_options, data_directory
7
+ from digichem.config import Auto_type, get_config
8
+ from digichem.config.parse import Config_parser
9
+
10
+ def test_config_save(digichem_options, tmpdir):
11
+ """Can we save config options to file?"""
12
+ # First, check an exception is thrown if we try and save to a non-existent dir:
13
+ # with pytest.raises(Exception):
14
+ # digichem_options.save(Path(tmpdir, "foobar", "conf.yaml"))
15
+
16
+ # Now save properly.
17
+ save_file = Path(tmpdir, "conf.yaml")
18
+ digichem_options.save(save_file)
19
+
20
+ # Open the file and read it back.
21
+ with open(save_file) as file:
22
+ data = yaml.safe_load(file)
23
+
24
+ # Check the data is the same.
25
+ assert digichem_options.dump() == data
26
+
27
+ @pytest.mark.parametrize("var, typeis", [
28
+ ["1", int],
29
+ ["1.1", float],
30
+ ["one", str]
31
+ ], ids=["int", "float", "str"])
32
+ def test_autotype(var, typeis):
33
+ """Does autotype work?"""
34
+ assert type(Auto_type(var)) is typeis
35
+
36
+ def test_string_parser():
37
+ """Can we parse config options from a string."""
38
+ Config_parser("foo: bar").load() == {"foo": "bar"}
39
+
40
+ def test_bad_string_parser():
41
+ """Can we not parse config options from a malformed string."""
42
+ with pytest.raises(Exception):
43
+ Config_parser("foo: {bar").load()
44
+
45
+ def test_del_option(digichem_options):
46
+ """Can we reset an option to its default by deleting it?"""
47
+ options = copy.deepcopy(digichem_options)
48
+ # First, set a value.
49
+ options.alignment = "FAP"
50
+
51
+ # Delete and check.
52
+ del options.alignment
53
+ assert options.alignment == "MIN"
54
+
55
+ def test_config_merge():
56
+ """Can we set additional options from a string?"""
57
+ conf = get_config(
58
+ clear_cache = True,
59
+ extra_config_strings = ["alignment: FAP"]
60
+ )
61
+
62
+ assert conf.alignment == "FAP"
63
+ get_config(clear_cache = True)
64
+
65
+ def test_config_file_merge(tmpdir):
66
+ """Can we set additional options from another file?"""
67
+ with open(Path(tmpdir, "conf.yaml"), "w") as conf_file:
68
+ yaml.dump({'alignment': 'FAP'}, conf_file)
69
+
70
+ conf = get_config(
71
+ clear_cache = True,
72
+ extra_config_files = [Path(tmpdir, "conf.yaml")]
73
+ )
74
+
75
+ assert conf.alignment == "FAP"
76
+
77
+ # Reset the config object.
78
+ get_config(clear_cache = True)