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,514 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numpy
|
|
3
|
+
import itertools
|
|
4
|
+
import scipy.constants
|
|
5
|
+
import scipy.signal
|
|
6
|
+
|
|
7
|
+
from digichem.misc.base import transpose
|
|
8
|
+
from digichem.exception.base import Digichem_exception
|
|
9
|
+
import digichem.result.excited_state
|
|
10
|
+
|
|
11
|
+
class Spectroscopy_graph_abc():
|
|
12
|
+
"""
|
|
13
|
+
ABC for classes used to plot Gaussians over spectroscopic results.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def coordinates(self):
|
|
18
|
+
"""
|
|
19
|
+
Get the coordinates around which this graph is plotted.
|
|
20
|
+
|
|
21
|
+
Note that this property may be transformed into different units, use base_coordinates for untransformed variant.
|
|
22
|
+
"""
|
|
23
|
+
raise NotImplementedError("Implement in subclass")
|
|
24
|
+
|
|
25
|
+
def peaks(self):
|
|
26
|
+
"""
|
|
27
|
+
Find peaks in the cumulative graph.
|
|
28
|
+
|
|
29
|
+
:return: A list of x,y coords that are peaks.
|
|
30
|
+
"""
|
|
31
|
+
coords = self.plot_cumulative_gaussian()
|
|
32
|
+
y_coords = [y for x,y in coords]
|
|
33
|
+
indexes = scipy.signal.find_peaks(y_coords, height = max(y_coords) * self.cutoff)[0]
|
|
34
|
+
return [coords[index] for index in indexes]
|
|
35
|
+
|
|
36
|
+
def plot_gaussian(self):
|
|
37
|
+
"""
|
|
38
|
+
Plot a gaussian distribution around a set of coordinates.
|
|
39
|
+
:return: A list of lists of tuples of (x, y) coordinates plotted by the gaussian function (one list per input coordinate).
|
|
40
|
+
"""
|
|
41
|
+
raise NotImplementedError("Implement in subclass")
|
|
42
|
+
|
|
43
|
+
def plot_cumulative_gaussian(self):
|
|
44
|
+
"""
|
|
45
|
+
Plot an additive gaussian distribution around a set of coordinates.
|
|
46
|
+
|
|
47
|
+
:return: A single list of tuples of (x, y) coordinates plotted by the gaussian function.
|
|
48
|
+
"""
|
|
49
|
+
# First, get our gaussian plots.
|
|
50
|
+
gplots = self.plot_gaussian()
|
|
51
|
+
|
|
52
|
+
# Sum our y values.
|
|
53
|
+
coords = {}
|
|
54
|
+
for plot in gplots:
|
|
55
|
+
for x, y in plot:
|
|
56
|
+
coords[x] = coords.get(x, 0) + y
|
|
57
|
+
|
|
58
|
+
# Now return as list of tuples.
|
|
59
|
+
return sorted(list(coords.items()), key = lambda coord: coord[0])
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def gaussian(self, a, b, c, x):
|
|
63
|
+
"""
|
|
64
|
+
An implementation of the gaussian function.
|
|
65
|
+
|
|
66
|
+
:param a: The maximum height of the peak.
|
|
67
|
+
:param b: The x position of the center of the peak.
|
|
68
|
+
:param c: The width of the peak.
|
|
69
|
+
:param x: The x-value to plot for.
|
|
70
|
+
:return: The corresponding y value.
|
|
71
|
+
"""
|
|
72
|
+
return a * math.exp(-( (x - b)**2 / (2 * c**2) ))
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def gaussian_x(self, a, b, c, y):
|
|
76
|
+
"""
|
|
77
|
+
An implementation of the gaussian function rearranged to find x.
|
|
78
|
+
|
|
79
|
+
:param a: The maximum height of the peak.
|
|
80
|
+
:param b: The x position of the center of the peak.
|
|
81
|
+
:param c: The width of the peak.
|
|
82
|
+
:param y: The y-value to plot for.
|
|
83
|
+
:return: A tuple of the two corresponding x values.
|
|
84
|
+
"""
|
|
85
|
+
# We have two solutions of the form x = ± d + b
|
|
86
|
+
# Calculate d first.
|
|
87
|
+
try:
|
|
88
|
+
d = math.sqrt( -math.log( y/a ) * 2 * c **2 )
|
|
89
|
+
except ZeroDivisionError:
|
|
90
|
+
# a (max height) is zero; the intensity is zero.
|
|
91
|
+
# We could instead return b?
|
|
92
|
+
raise Digichem_exception("'{}' cannot calculate Gaussian function limits; a (intensity) is zero".format(self.__name__))
|
|
93
|
+
|
|
94
|
+
# Now return our two solutions.
|
|
95
|
+
return (-d + b, d + b)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def fwhm_to_c(self, fwhm):
|
|
99
|
+
"""
|
|
100
|
+
Convert a full width at half maximum to the corresponding c-value in the Gaussian function.
|
|
101
|
+
|
|
102
|
+
:param fwhm: The desired full width at half maximum (in nm).
|
|
103
|
+
:return: The c-value.
|
|
104
|
+
"""
|
|
105
|
+
return fwhm / (2 * math.sqrt(2 * math.log(2)))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Spectroscopy_graph(Spectroscopy_graph_abc):
|
|
109
|
+
"""
|
|
110
|
+
Top level class for graphing spectroscopy results (energy vs intensity).
|
|
111
|
+
|
|
112
|
+
For generating pictures of these graphs, see digichem.image.spectroscopy
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, coordinates, fwhm, resolution = 1, cutoff = 0.01, adjust_zero = False):
|
|
116
|
+
"""
|
|
117
|
+
Constructor for Spectroscopy_graph objects
|
|
118
|
+
|
|
119
|
+
:param coordinates: A list of (energy, intensity) tuples to plot. The units of energy and intensity are irrelevant here (but should be consistent). Note that coordinates with 0 intensity will be removed.
|
|
120
|
+
:param fwhm: The full-width at half-maximum to plot peaks with (in units of the x axis).
|
|
121
|
+
:param resolution: The spacing (or step-size) between points to plot using the gaussian function, in units of the x-axis.
|
|
122
|
+
:param cutoff: The minimum y value to plot using the gaussian function, as a fraction of the intensity.
|
|
123
|
+
:param adjust_zero: If True and all y values are 0, set all y values to 1 (so that something can be plotted).
|
|
124
|
+
"""
|
|
125
|
+
# We save our coordinates under two properties.
|
|
126
|
+
# base_coordinates are untransformed.
|
|
127
|
+
# coordinates (which is by default the same as base_coordinates) are transformed.
|
|
128
|
+
self.false_intensity = False
|
|
129
|
+
# Set arb y value if we've been asked to.
|
|
130
|
+
if adjust_zero:
|
|
131
|
+
# Check if all the y axis is zero.
|
|
132
|
+
if all([y == 0 for x,y in coordinates]):
|
|
133
|
+
coordinates = [(x, 1) for x, y in coordinates]
|
|
134
|
+
self.false_intensity = True
|
|
135
|
+
|
|
136
|
+
self.base_coordinates = [(x, y) for x,y in coordinates if y != 0]
|
|
137
|
+
self.fwhm = fwhm
|
|
138
|
+
self.resolution = resolution
|
|
139
|
+
self.cutoff = cutoff
|
|
140
|
+
|
|
141
|
+
# Caches
|
|
142
|
+
self._gaussians = []
|
|
143
|
+
self._cumulative_gaussians = []
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_vibrations(self, vibrations, *args, **kwargs):
|
|
147
|
+
"""
|
|
148
|
+
Alternative constructor from a Vibrations_list object.
|
|
149
|
+
"""
|
|
150
|
+
return self([(vibration.frequency, vibration.intensity) for vibration in vibrations], *args, **kwargs)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_excited_states(self, excited_states, *args, **kwargs):
|
|
154
|
+
"""
|
|
155
|
+
An alternative constructor that takes a list of excited states as argument.
|
|
156
|
+
|
|
157
|
+
:param excited_states: An Excited_states_list object to construct from.
|
|
158
|
+
:param adjust_zero: If all the intensities of the given excited states are zero, whether to arbitrarily set the y coords to 1.
|
|
159
|
+
:param **kwargs: Passed to the real constructor.
|
|
160
|
+
"""
|
|
161
|
+
coords = [(excited_state.energy, excited_state.oscillator_strength if excited_state.oscillator_strength is not None else 0) for excited_state in excited_states]
|
|
162
|
+
return self(coords, *args, **kwargs)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def coordinates(self):
|
|
166
|
+
"""
|
|
167
|
+
Get the coordinates around which this graph is plotted.
|
|
168
|
+
|
|
169
|
+
Note that this property may be transformed into different units, use base_coordinates for untransformed variant.
|
|
170
|
+
"""
|
|
171
|
+
return self.base_coordinates
|
|
172
|
+
|
|
173
|
+
def plot_gaussian(self, refresh = False):
|
|
174
|
+
"""
|
|
175
|
+
Plot a gaussian distribution around a set of coordinates.
|
|
176
|
+
:return: A list of lists of tuples of (x, y) coordinates plotted by the gaussian function (one list per input coordinate).
|
|
177
|
+
"""
|
|
178
|
+
if len(self._gaussians) == 0 or refresh:
|
|
179
|
+
self._gaussians = self.get_gaussian()
|
|
180
|
+
|
|
181
|
+
return self._gaussians
|
|
182
|
+
|
|
183
|
+
def get_gaussian(self):
|
|
184
|
+
# First, determine our c value.
|
|
185
|
+
c = self.fwhm_to_c(self.fwhm)
|
|
186
|
+
|
|
187
|
+
# Next, determine the limits in which we'll plot.
|
|
188
|
+
limits = self.gaussian_limits()
|
|
189
|
+
|
|
190
|
+
# Apply rounding to nearest resolution step cover up floating-point errors.
|
|
191
|
+
points = [self.resolution *round(point / self.resolution) for point in numpy.linspace(*limits)]
|
|
192
|
+
|
|
193
|
+
# Plot and return.
|
|
194
|
+
digichem.log.get_logger().debug(
|
|
195
|
+
"Plotting gaussian peaks from {:0.2f} to {:0.2f} with a step size of {} ({} total points) for {} peaks ({} total iterations)".format(
|
|
196
|
+
limits[0],
|
|
197
|
+
limits[1],
|
|
198
|
+
self.resolution,
|
|
199
|
+
limits[2],
|
|
200
|
+
len(self.base_coordinates),
|
|
201
|
+
len(self.base_coordinates) * limits[2]
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
gaussians = [
|
|
205
|
+
[(x, self.gaussian(a, b, c, x)) for x in points]
|
|
206
|
+
for b, a in self.base_coordinates
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
return gaussians
|
|
210
|
+
|
|
211
|
+
def plot_cumulative_gaussian(self, refresh = False):
|
|
212
|
+
"""
|
|
213
|
+
Plot an additive gaussian distribution around a set of coordinates.
|
|
214
|
+
|
|
215
|
+
:return: A single list of tuples of (x, y) coordinates plotted by the gaussian function.
|
|
216
|
+
"""
|
|
217
|
+
if len(self._cumulative_gaussians) == 0 or refresh:
|
|
218
|
+
self._cumulative_gaussians = self.get_cumulative_gaussian()
|
|
219
|
+
|
|
220
|
+
return self._cumulative_gaussians
|
|
221
|
+
|
|
222
|
+
def get_cumulative_gaussian(self):
|
|
223
|
+
# First, determine our c value.
|
|
224
|
+
c = self.fwhm_to_c(self.fwhm)
|
|
225
|
+
|
|
226
|
+
# Next, determine the limits in which we'll plot.
|
|
227
|
+
limits = self.gaussian_limits()
|
|
228
|
+
|
|
229
|
+
# Apply rounding to nearest resolution step cover up floating-point errors.
|
|
230
|
+
points = [self.resolution *round(point / self.resolution) for point in numpy.linspace(*limits)]
|
|
231
|
+
|
|
232
|
+
# Plot and return.
|
|
233
|
+
digichem.log.get_logger().debug("Plotting cumulative gaussian peaks from {:0.2f} to {:0.2f} with a step size of {} ({} total points) for {} peaks ({} total iterations)".format(limits[0], limits[1], self.resolution, limits[2], len(self.base_coordinates), len(self.base_coordinates) * limits[2]))
|
|
234
|
+
gaussian = [
|
|
235
|
+
(x, sum((self.gaussian(a, b, c, x) for b, a in self.base_coordinates))) for x in points
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
return gaussian
|
|
239
|
+
|
|
240
|
+
def gaussian_limits(self):
|
|
241
|
+
"""
|
|
242
|
+
Determine min and max x limits to plot a gaussian function.
|
|
243
|
+
|
|
244
|
+
:param c: The width of the peak.
|
|
245
|
+
:param cutoff: The minimum y value to plot using the gaussian function, as the fraction of a.
|
|
246
|
+
:param resolution: The spacing between points to plot using the gaussian function, in units of the x-axis.
|
|
247
|
+
:return: A tuple of (minlim, maxlim, num) where minlim is the most negative value, maxlim the most positive and num the integer number of points to plot between them to achieve resolution.
|
|
248
|
+
"""
|
|
249
|
+
# Calculate limits for each set of coordinates given to us.
|
|
250
|
+
all_limits = [self.gaussian_x(y, x, self.fwhm_to_c(self.fwhm), self.cutoff * y) for x, y in self.base_coordinates]
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
limits = (min(itertools.chain.from_iterable(all_limits)), max(itertools.chain.from_iterable(all_limits)))
|
|
254
|
+
|
|
255
|
+
except ValueError:
|
|
256
|
+
if len(list(itertools.chain.from_iterable(all_limits))) == 0:
|
|
257
|
+
# Nothing to plot.
|
|
258
|
+
raise Digichem_exception("'{}' cannot plot spectrum; there are no values".format(type(self).__name__))
|
|
259
|
+
else:
|
|
260
|
+
raise
|
|
261
|
+
|
|
262
|
+
# Align our start and end points to an integer multiple of resolution.
|
|
263
|
+
# This is necessary to facilitate easy summation of gaussians (because they all align to the same grid,
|
|
264
|
+
# assuming the same value of resolution)
|
|
265
|
+
start = math.floor(limits[0] / self.resolution) * self.resolution
|
|
266
|
+
end = math.ceil(limits[1] / self.resolution) * self.resolution
|
|
267
|
+
|
|
268
|
+
# Now we need to generate the x values which we'll plot for.
|
|
269
|
+
# This is a little more complicated than it needs to be because there's no simple range() for floats.
|
|
270
|
+
# Calculate the number of points (and round up).
|
|
271
|
+
num_points = round( math.fabs(end - start) / self.resolution)
|
|
272
|
+
|
|
273
|
+
# Extend our limits so they are a clean multiple of num_points.
|
|
274
|
+
limits = (start, start + num_points * self.resolution)
|
|
275
|
+
|
|
276
|
+
return (limits[0], limits[1], num_points +1)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class Absorption_emission_graph(Spectroscopy_graph):
|
|
280
|
+
"""
|
|
281
|
+
Class for graphing absorption/emission spectra in nm.
|
|
282
|
+
|
|
283
|
+
Use the normal Spectroscopy_graph object for plotting in eV.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def __init__(self, *args, use_jacobian = True, **kwargs):
|
|
287
|
+
"""
|
|
288
|
+
Constructor for Absorption_emission_graph objects.
|
|
289
|
+
|
|
290
|
+
:param false_intensity: A flag to indicate that the intensity units (y-axis) are arbitrary.
|
|
291
|
+
:param use_jacobian: Whether to use the jacobian transform to scale the y axis.
|
|
292
|
+
"""
|
|
293
|
+
# Get our x,y values from our excited states.
|
|
294
|
+
super().__init__(*args, **kwargs)
|
|
295
|
+
self.use_jacobian = use_jacobian
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def coordinates(self):
|
|
299
|
+
"""
|
|
300
|
+
Get the coordinates around which this graph is plotted.
|
|
301
|
+
|
|
302
|
+
Note that this property may be transformed into different units, use base_coordinates for untransformed variant.
|
|
303
|
+
"""
|
|
304
|
+
return self.nm_coordinates
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def nm_coordinates(self):
|
|
308
|
+
"""
|
|
309
|
+
Get the list of coordinates of this graph scaled to nm.
|
|
310
|
+
"""
|
|
311
|
+
return [self.energy_to_wavelength(coord, self.use_jacobian) for coord in self.base_coordinates]
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def energy_to_wavelength(self, coord, use_jacobian):
|
|
315
|
+
"""
|
|
316
|
+
Convert a pair of x,y coordinates in ev to nm.
|
|
317
|
+
|
|
318
|
+
:param coord: A tuple of (eV, f).
|
|
319
|
+
:return: A tuple of (nm, i), where i is intensity.
|
|
320
|
+
"""
|
|
321
|
+
x, y = coord
|
|
322
|
+
return (digichem.result.excited_state.Excited_state.energy_to_wavelength(x), self.jacobian(x, y) if use_jacobian else y)
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def jacobian(self, E, f_E):
|
|
326
|
+
"""
|
|
327
|
+
An implementation of the jacobian transform that scales intensity in energy units to intensity in wavelength units.
|
|
328
|
+
|
|
329
|
+
See J. Phys. Chem. Lett. 2014, 5, 20, 3497 for why this is necessary.
|
|
330
|
+
|
|
331
|
+
Note that the jacobian transform will maintain the area under the curve regardless of x units (nm or x).
|
|
332
|
+
Sadly, this has the consequence of mangling the intensity units (it becomes tiny; an oscillator strength of 1 at 3 eV becomes 1.163e-12).
|
|
333
|
+
"""
|
|
334
|
+
return ((E * scipy.constants.electron_volt)**2 * f_E) / (scipy.constants.Planck * scipy.constants.c)
|
|
335
|
+
|
|
336
|
+
def plot_gaussian(self):
|
|
337
|
+
"""
|
|
338
|
+
Plot a gaussian distribution around our excited state energies.
|
|
339
|
+
|
|
340
|
+
:return: A list of lists of tuples of (x, y) coordinates plotted by the gaussian function (one list per input coordinate).
|
|
341
|
+
"""
|
|
342
|
+
# All we need to do over our parent is convert x values from e to wavelength.
|
|
343
|
+
# And scale y values using the jacobian transform.
|
|
344
|
+
return [[self.energy_to_wavelength(coord, self.use_jacobian) for coord in plot] for plot in super().plot_gaussian()]
|
|
345
|
+
|
|
346
|
+
def plot_cumulative_gaussian(self):
|
|
347
|
+
"""
|
|
348
|
+
Plot an additive gaussian distribution around a set of coordinates.
|
|
349
|
+
|
|
350
|
+
:return: A single list of tuples of (x, y) coordinates plotted by the gaussian function.
|
|
351
|
+
"""
|
|
352
|
+
return [self.energy_to_wavelength(coord, self.use_jacobian) for coord in super().plot_cumulative_gaussian()]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# TODO: Find this a better home?
|
|
356
|
+
def unpack_coupling(couplings, satellite_threshold = 0.02):
|
|
357
|
+
"""
|
|
358
|
+
Unpack a nested dict of dict of NMR_group_spin_coupling objects into a single, ordered dict.
|
|
359
|
+
|
|
360
|
+
The keys of the returned dict will be a tuple of (foreign_atom_group, foreign_isotope).
|
|
361
|
+
"""
|
|
362
|
+
couplings = {
|
|
363
|
+
(coupling_group, coupling_isotope): isotope_coupling for coupling_group, atom_dict in couplings.items() for coupling_isotope, isotope_coupling in atom_dict.items()
|
|
364
|
+
if coupling_group.element[coupling_isotope].abundance / 100 > satellite_threshold
|
|
365
|
+
}
|
|
366
|
+
# Sort couplings.
|
|
367
|
+
couplings = dict(sorted(couplings.items(), key = lambda coupling: abs(coupling[1].total), reverse = True))
|
|
368
|
+
|
|
369
|
+
return couplings
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class NMR_graph(Spectroscopy_graph):
|
|
373
|
+
"""
|
|
374
|
+
A class for plotting a single NMR peak.
|
|
375
|
+
|
|
376
|
+
A 'single' NMR peak here refers to the signal that would be observed from only one atom (or atom group).
|
|
377
|
+
For plotting an entire spectrum, use a Combined_graph of NMR_graph objects.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
def multiplicity(self, atom_group, coupling, satellite_threshold = 0.02):
|
|
381
|
+
"""
|
|
382
|
+
Determine the multiplicity of this peak.
|
|
383
|
+
|
|
384
|
+
Note that the returned multiplicity might not match exactly the observed multiplicity of the peak
|
|
385
|
+
due to line-broadening and overlapping signals.
|
|
386
|
+
|
|
387
|
+
:param atom_group: The atom_group that this peak corresponds to.
|
|
388
|
+
:param coupling: NMR_group_spin_coupling objects between this atom_group and other groups.
|
|
389
|
+
"""
|
|
390
|
+
# First, determine how many peaks are visible.
|
|
391
|
+
peaks = self.peaks()
|
|
392
|
+
|
|
393
|
+
# Filter out peaks which are significantly smaller than the tallest peak.
|
|
394
|
+
max_peak = max(transpose(peaks, 2)[1])
|
|
395
|
+
peaks = [peak for peak in peaks if peak[1] > max_peak * satellite_threshold]
|
|
396
|
+
|
|
397
|
+
if len(peaks) == 1:
|
|
398
|
+
return [{"symbol": "s", "number": 1, "multiplicity": "singlet"}]
|
|
399
|
+
|
|
400
|
+
# A list of multiplicity dicts.
|
|
401
|
+
mults = []
|
|
402
|
+
total_peaks = 1
|
|
403
|
+
coupling_index = 0
|
|
404
|
+
|
|
405
|
+
# Get couplings.
|
|
406
|
+
couplings = unpack_coupling(coupling)
|
|
407
|
+
couplings = list(couplings.values())
|
|
408
|
+
|
|
409
|
+
# Unless coupling has been calculated for all available isotopes for each atom group (unlikely),
|
|
410
|
+
# there will be one additional peak for each atom group from non-NMR active nuclei.
|
|
411
|
+
# Make sure we don't count this peak multiple times.
|
|
412
|
+
#
|
|
413
|
+
# For atoms in which the majority of the abundance is already accounted for (1H, for example),
|
|
414
|
+
# exclude the residual peak.
|
|
415
|
+
residual_isotope_peaks = set(
|
|
416
|
+
[atom_group for atom_group, isotopes in coupling.items() if sum((atom_group.element[isotope].abundance for isotope in isotopes.keys())) / 100 > satellite_threshold]
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# We will keep requesting more splitting until we are able to account for all the peaks we can see.
|
|
420
|
+
while len(peaks) > total_peaks:
|
|
421
|
+
# Get the next coupling available.
|
|
422
|
+
try:
|
|
423
|
+
group_coupling = couplings[coupling_index]
|
|
424
|
+
|
|
425
|
+
except IndexError:
|
|
426
|
+
# Ran out of coupling.
|
|
427
|
+
break
|
|
428
|
+
|
|
429
|
+
other_group = group_coupling.groups[group_coupling.other(atom_group)]
|
|
430
|
+
residual = other_group not in residual_isotope_peaks
|
|
431
|
+
multiplicity = group_coupling.multiplicity(atom_group)
|
|
432
|
+
residual_isotope_peaks.add(other_group)
|
|
433
|
+
|
|
434
|
+
mults.append(multiplicity)
|
|
435
|
+
num = multiplicity['number']
|
|
436
|
+
if residual:
|
|
437
|
+
num += 1
|
|
438
|
+
|
|
439
|
+
total_peaks *= num
|
|
440
|
+
|
|
441
|
+
# Update counter.
|
|
442
|
+
coupling_index += 1
|
|
443
|
+
|
|
444
|
+
return mults
|
|
445
|
+
|
|
446
|
+
@classmethod
|
|
447
|
+
def from_nmr(self, nmr_peaks, *args, **kwargs):
|
|
448
|
+
"""
|
|
449
|
+
An alternative constructor that takes a list of simulated NMR peaks as argument.
|
|
450
|
+
|
|
451
|
+
:param nmr_peaks: Simulated NMR peaks. See result.nmr.NMR_spectrometer for how to obtain this data.
|
|
452
|
+
"""
|
|
453
|
+
return self(nmr_peaks, *args, **kwargs)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class Combined_graph(Spectroscopy_graph_abc):
|
|
457
|
+
"""
|
|
458
|
+
A class for plotting one graph from multiple, individual graphs.
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
def __init__(self, graphs):
|
|
462
|
+
"""
|
|
463
|
+
Constructor for Combined_graph objects.
|
|
464
|
+
|
|
465
|
+
:param graphs: A dictionary of individual Spectroscopy_graph objects to combine.
|
|
466
|
+
"""
|
|
467
|
+
self.graphs = graphs
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def cutoff(self):
|
|
471
|
+
"""
|
|
472
|
+
"""
|
|
473
|
+
return max((graph.cutoff for graph in self.graphs.values()))
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def fwhm(self):
|
|
477
|
+
"""
|
|
478
|
+
"""
|
|
479
|
+
return sum((graph.fwhm for graph in self.graphs.values())) / len(self.graphs)
|
|
480
|
+
|
|
481
|
+
@classmethod
|
|
482
|
+
def from_nmr(self, grouped_peaks, *args, **kwargs):
|
|
483
|
+
"""
|
|
484
|
+
Construct a Combined_graph from a dictionary of grouped NMR peaks
|
|
485
|
+
|
|
486
|
+
:param grouped_peaks: A dictionary of lists of NMR peaks. Each key in the dictionary should correspond to one atom group.
|
|
487
|
+
"""
|
|
488
|
+
return self(
|
|
489
|
+
{
|
|
490
|
+
peak_key: NMR_graph.from_nmr(peaks, *args, **kwargs) for peak_key, peaks in grouped_peaks.items()
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def coordinates(self):
|
|
496
|
+
"""
|
|
497
|
+
Get the coordinates around which this graph is plotted.
|
|
498
|
+
|
|
499
|
+
Note that this property may be transformed into different units, use base_coordinates for untransformed variant.
|
|
500
|
+
"""
|
|
501
|
+
return list(itertools.chain(*[graph.coordinates for graph in self.graphs.values()]))
|
|
502
|
+
|
|
503
|
+
def plot_gaussian(self):
|
|
504
|
+
"""
|
|
505
|
+
Plot a gaussian distribution around a set of coordinates.
|
|
506
|
+
:return: A list of lists of tuples of (x, y) coordinates plotted by the gaussian function (one list per input coordinate).
|
|
507
|
+
"""
|
|
508
|
+
# Because this class represents one additional layer of abstractions,
|
|
509
|
+
# we'll combine the results returned by each sub graph together, to obtain a single nested list.
|
|
510
|
+
return [
|
|
511
|
+
graph.plot_cumulative_gaussian()
|
|
512
|
+
for graph in self.graphs.values()
|
|
513
|
+
]
|
|
514
|
+
|