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,797 @@
|
|
|
1
|
+
from matplotlib import ticker
|
|
2
|
+
import matplotlib.collections
|
|
3
|
+
import statistics
|
|
4
|
+
import adjustText
|
|
5
|
+
|
|
6
|
+
from digichem.exception.base import File_maker_exception
|
|
7
|
+
from digichem.result.spectroscopy import Spectroscopy_graph,\
|
|
8
|
+
Absorption_emission_graph, unpack_coupling
|
|
9
|
+
from digichem.image.graph import Graph_image_maker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Spectroscopy_graph_maker(Graph_image_maker):
|
|
13
|
+
"""
|
|
14
|
+
A graph type for displaying spectroscopy data (UV-vis, IR etc.).
|
|
15
|
+
"""
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
output,
|
|
19
|
+
graph,
|
|
20
|
+
*,
|
|
21
|
+
# Keyword only arguments from here:
|
|
22
|
+
|
|
23
|
+
# Options that control what to show in the graph.
|
|
24
|
+
plot_bars = True,
|
|
25
|
+
plot_peaks = False,
|
|
26
|
+
plot_cumulative_peak = True,
|
|
27
|
+
|
|
28
|
+
# Options controlling the axes limits.
|
|
29
|
+
x_padding = 50,
|
|
30
|
+
peak_cutoff = 0.01,
|
|
31
|
+
x_limits = 'auto',
|
|
32
|
+
y_limits = 'auto',
|
|
33
|
+
**kwargs
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Constructor for spectroscopy graphs.
|
|
37
|
+
|
|
38
|
+
:param output: A path to an output file to write to. The extension of this path is used to determine the format of the file (eg, png, jpeg).
|
|
39
|
+
:param graph: An instance of a digichem.result.spectroscopy.Spectroscopy_graph that contains data to plot.
|
|
40
|
+
:param fwhm: The desired full width at half maximum of the peaks that we are going to plot.
|
|
41
|
+
:param gaussian_cutoff: The minimum y value to plot using the gaussian function, as the fraction of the max intensity.
|
|
42
|
+
:param gaussian_resolution: The spacing between points to plot using the gaussian function, in units of the x-axis.
|
|
43
|
+
:param plot_bars: If True, vertical bars are plotted on the graph for each excited state.
|
|
44
|
+
:param plot_peaks: If True, Gaussian peaks are plotted for each excited state.
|
|
45
|
+
:param plot_cumulative_peak: If True, a cumulative graph is plotted from gaussian peaks from all excited states.
|
|
46
|
+
:param x_padding: An amount, in units of the x-axis, to extend beyond the lowest & highest energy peak (so the graph is framed). Note that the graph will never extend beyond 0.
|
|
47
|
+
:param y_cutoff: The minimum y value to include when using auto x limits (the x axis will extend
|
|
48
|
+
:param x_limits_method: String controlling how the x axis limits are set. Options are 'auto' for automatic scaling, showing all plotted peaks. Alternatively, a tuple of (x_min, x_max) which will be used directly as axis limits.
|
|
49
|
+
:param y_limits_method: String controlling how the y axis limits are set. Options are 'auto' for automatic scaling. Alternatively, a tuple of (y_min, y_max) which will be used directly as axis limits.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
# Call our image maker parent.
|
|
53
|
+
super().__init__(output, **kwargs)
|
|
54
|
+
|
|
55
|
+
# Save our graph object (holds coordinates to plot).
|
|
56
|
+
self.graph = graph
|
|
57
|
+
|
|
58
|
+
# Options controlling what to plot.
|
|
59
|
+
self.should_plot_bars = plot_bars
|
|
60
|
+
self.should_plot_peaks = plot_peaks
|
|
61
|
+
self.should_plot_cumulative_peak = plot_cumulative_peak
|
|
62
|
+
|
|
63
|
+
self.init_image_width = 10
|
|
64
|
+
self.init_image_height = 5.26
|
|
65
|
+
|
|
66
|
+
# Axis scaling.
|
|
67
|
+
self.x_padding = x_padding
|
|
68
|
+
self.peak_cutoff = peak_cutoff
|
|
69
|
+
self.x_limits_method = x_limits
|
|
70
|
+
self.y_limits_method = y_limits
|
|
71
|
+
|
|
72
|
+
# Line-widths.
|
|
73
|
+
self.linewidth = 1
|
|
74
|
+
self.cumulative_linewidth = 2
|
|
75
|
+
self.column_linewidth = 1.5
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def fwhm(self):
|
|
79
|
+
return self.graph.fwhm
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def transpose(self, coordinates):
|
|
83
|
+
"""
|
|
84
|
+
Transpose a list of coordinates from [(x1,y1), (x2,y2)...] to [[x1, x2...], [y1, y2...]]
|
|
85
|
+
"""
|
|
86
|
+
return list(map(list, zip(*coordinates)))
|
|
87
|
+
|
|
88
|
+
def plot(self):
|
|
89
|
+
"""
|
|
90
|
+
Plot the contents of our graph.
|
|
91
|
+
"""
|
|
92
|
+
# Make sure we actually have something to plot.
|
|
93
|
+
if len(self.graph.coordinates) == 0:
|
|
94
|
+
# We have nothing to plot; we could plot an empty graph (and maybe should?), but for now we'll stop.
|
|
95
|
+
raise File_maker_exception(self, "cannot plot spectrum; there are no values with intensity above 0")
|
|
96
|
+
|
|
97
|
+
# What we do next depends on what options we've been given.
|
|
98
|
+
if self.should_plot_bars:
|
|
99
|
+
self.plot_columns()
|
|
100
|
+
# If we aren't going to plot any curves, plot some invisible points so we get automatic axis scaling.
|
|
101
|
+
if not self.should_plot_peaks and not self.should_plot_cumulative_peak:
|
|
102
|
+
self.plot_hidden_points()
|
|
103
|
+
|
|
104
|
+
if self.should_plot_cumulative_peak:
|
|
105
|
+
self.plot_cumulative_line()
|
|
106
|
+
|
|
107
|
+
if self.should_plot_peaks:
|
|
108
|
+
self.plot_lines()
|
|
109
|
+
|
|
110
|
+
def plot_columns(self):
|
|
111
|
+
"""
|
|
112
|
+
Plot vertical columns for each plot on the graph we are building.
|
|
113
|
+
"""
|
|
114
|
+
columns = [[(x, 0) , (x, y)] for x, y in self.graph.coordinates]
|
|
115
|
+
self.axes.add_collection(matplotlib.collections.LineCollection(columns, linewidths = self.column_linewidth, colors = (0,0,0)))
|
|
116
|
+
|
|
117
|
+
def plot_hidden_points(self):
|
|
118
|
+
"""
|
|
119
|
+
Plot invisible points to take advantage of matplotlib auto scaling.
|
|
120
|
+
"""
|
|
121
|
+
# Plot. Set size (s) to zero so we don't see our points, but we still get good automatic scaling.
|
|
122
|
+
self.axes.scatter(*self.transpose(self.graph.coordinates), s=0)
|
|
123
|
+
|
|
124
|
+
def plot_lines(self):
|
|
125
|
+
"""
|
|
126
|
+
Plot x and y values as individual lines on the graph we are building.
|
|
127
|
+
"""
|
|
128
|
+
for plot in self.graph.plot_gaussian():
|
|
129
|
+
data = self.transpose(plot)
|
|
130
|
+
self.axes.plot(*data, 'C0-', linewidth = self.linewidth)
|
|
131
|
+
|
|
132
|
+
def plot_cumulative_line(self):
|
|
133
|
+
"""
|
|
134
|
+
Plot x and y values as a single line on the graph we are building.
|
|
135
|
+
"""
|
|
136
|
+
data = self.transpose(self.graph.plot_cumulative_gaussian())
|
|
137
|
+
self.axes.plot(*data, 'C0-', linewidth = self.cumulative_linewidth)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def peaks(self):
|
|
141
|
+
"""
|
|
142
|
+
A list of peaks (in x units).
|
|
143
|
+
"""
|
|
144
|
+
return [x for x,y in self.graph.peaks()]
|
|
145
|
+
|
|
146
|
+
def selected_peaks(self, decimals = 0, number = None):
|
|
147
|
+
"""
|
|
148
|
+
A unique list of sorted peaks, rounded to a given number of decimal points.
|
|
149
|
+
|
|
150
|
+
:param decimals: The number of decimal points to round to.
|
|
151
|
+
:param number: The number of peaks to return (the most intense peaks will be returned first).
|
|
152
|
+
:returns: A list of ordered peaks as floats (if decimals > 0) or ints.
|
|
153
|
+
"""
|
|
154
|
+
if number is not None:
|
|
155
|
+
peaks = self.graph.peaks()
|
|
156
|
+
peaks.sort(key = lambda coord: coord[1])
|
|
157
|
+
peaks = [x for x,y in peaks[-number:]]
|
|
158
|
+
|
|
159
|
+
else:
|
|
160
|
+
peaks = self.peaks
|
|
161
|
+
|
|
162
|
+
return sorted(list(set([round(peak, decimals) if decimals > 0 else int(peak) for peak in peaks])))
|
|
163
|
+
|
|
164
|
+
def auto_x_limits(self):
|
|
165
|
+
"""
|
|
166
|
+
Limit the X axis so all plotted peaks are visible.
|
|
167
|
+
"""
|
|
168
|
+
# We need to get a list of all peaks that are above our cutoff point.
|
|
169
|
+
# First determine our highest point.
|
|
170
|
+
highest_point = max(self.transpose(self.graph.coordinates)[1])
|
|
171
|
+
|
|
172
|
+
# Now filter by a fraction of that amount.
|
|
173
|
+
visible_x_values = [x for x, y in self.graph.plot_cumulative_gaussian() if y >= (highest_point * self.peak_cutoff)]
|
|
174
|
+
|
|
175
|
+
# Now we can just get our min-max, remembering to include our x-padding, and never extending beyond 0 (because negative energy has no meaning in this context(?)).
|
|
176
|
+
self.axes.set_xlim(max(min(visible_x_values) - self.x_padding, 0), max(visible_x_values) + self.x_padding)
|
|
177
|
+
|
|
178
|
+
def auto_y_limits(self):
|
|
179
|
+
"""
|
|
180
|
+
Limit the y axis so all plotted peaks are visible.
|
|
181
|
+
"""
|
|
182
|
+
# Use matplotlib autoscaling.
|
|
183
|
+
self.axes.autoscale(enable = True, axis = 'y')
|
|
184
|
+
|
|
185
|
+
# Clamp to 0 -> pos.
|
|
186
|
+
self.axes.set_ylim(0, self.axes.get_ylim()[1])
|
|
187
|
+
|
|
188
|
+
def adjust_axes(self):
|
|
189
|
+
"""
|
|
190
|
+
Adjust the axes of our graph.
|
|
191
|
+
|
|
192
|
+
This method is called automatically as part of the make() method.
|
|
193
|
+
"""
|
|
194
|
+
super().adjust_axes()
|
|
195
|
+
|
|
196
|
+
# Call the appropriate scaling method depending on the value of the limits_method.
|
|
197
|
+
for axis in ("x", "y"):
|
|
198
|
+
method = getattr(self, axis + "_limits_method")
|
|
199
|
+
if method == "auto":
|
|
200
|
+
# Call the automatic function.
|
|
201
|
+
getattr(self, "auto_{}_limits".format(axis))()
|
|
202
|
+
|
|
203
|
+
elif isinstance(method, tuple) or isinstance(method, list):
|
|
204
|
+
# Manual, explicit limits.
|
|
205
|
+
getattr(self.axes, "set_{}lim".format(axis))(method[0], method[1])
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
# Don't recognise method.
|
|
209
|
+
raise ValueError("Unknown {} limits method '{}'".format(axis, method))
|
|
210
|
+
|
|
211
|
+
# Use matplotlib to automatically layout our graph.
|
|
212
|
+
self.figure.tight_layout()
|
|
213
|
+
|
|
214
|
+
# Get a constant scale on our x axis.
|
|
215
|
+
self.constant_scale(0, self.inch_per_x)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Absorption_emission_graph_maker(Spectroscopy_graph_maker):
|
|
219
|
+
"""
|
|
220
|
+
A graph for displaying simulated absorptions.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
# The key/name of where our options are stored in the main config.
|
|
224
|
+
options_name = None
|
|
225
|
+
|
|
226
|
+
def __init__(self, output, graph, *args, **kwargs):
|
|
227
|
+
"""
|
|
228
|
+
Constructor for UV-Vis type absorption graphs.
|
|
229
|
+
|
|
230
|
+
:param output: A path to an output file to write to. The extension of this path is used to determine the format of the file (eg, png, jpeg).
|
|
231
|
+
:param graph: An instance of a digichem.result.spectroscopy.Spectroscopy_graph that contains data to plot.
|
|
232
|
+
:param adjust_zero: If all the intensities of the given excited states are zero, whether to arbitrarily set the y coords to 1.
|
|
233
|
+
:param peak_cutoff: The minimum oscillator strength a peak must have to be drawn, as a fraction of the tallest peak. Set to 0 for no cutoff.
|
|
234
|
+
:param max_width: The maximum width in pixels of the graph.
|
|
235
|
+
"""
|
|
236
|
+
# Call our parent.
|
|
237
|
+
super().__init__(output, graph, *args, **kwargs)
|
|
238
|
+
|
|
239
|
+
# The amount of space (in matplotlib inches) to allocate per unit of the x axis.
|
|
240
|
+
self.inch_per_x = 0.0192
|
|
241
|
+
|
|
242
|
+
# The DPI to save our image with.
|
|
243
|
+
self.output_dpi = 100
|
|
244
|
+
|
|
245
|
+
# Axes labels.
|
|
246
|
+
self.x_label = "Wavelength /nm"
|
|
247
|
+
if not self.graph.false_intensity:
|
|
248
|
+
self.y_label = "Intensity" if self.graph.use_jacobian else "Oscillator Strength"
|
|
249
|
+
else:
|
|
250
|
+
self.hide_y = True
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def from_options(self, output, *, excited_states, options, adjust_zero = True, **kwargs):
|
|
254
|
+
"""
|
|
255
|
+
Constructor that takes a dictionary of config like options.
|
|
256
|
+
"""
|
|
257
|
+
return self(
|
|
258
|
+
output,
|
|
259
|
+
Absorption_emission_graph.from_excited_states(excited_states, options[self.options_name]['fwhm'], options[self.options_name]['gaussian_resolution'], options[self.options_name]['gaussian_cutoff'], use_jacobian = options[self.options_name]['use_jacobian'], adjust_zero = adjust_zero),
|
|
260
|
+
**{key: value for key, value in options[self.options_name].items() if key not in ["gaussian_cutoff", "gaussian_resolution", "fwhm", "use_jacobian"]},
|
|
261
|
+
**kwargs
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def adjust_axes(self):
|
|
265
|
+
"""
|
|
266
|
+
Adjust the axes of our graph.
|
|
267
|
+
|
|
268
|
+
This method is called automatically as part of the make() method.
|
|
269
|
+
"""
|
|
270
|
+
# Change spacing of tick markers.
|
|
271
|
+
self.axes.xaxis.set_major_locator(ticker.MultipleLocator(50))
|
|
272
|
+
self.axes.xaxis.set_minor_locator(ticker.MultipleLocator(10))
|
|
273
|
+
|
|
274
|
+
# Call parent.
|
|
275
|
+
super().adjust_axes()
|
|
276
|
+
|
|
277
|
+
# Check to see if we've exceeded our max width.
|
|
278
|
+
if self.max_width is not None and (self.figure.get_size_inches()[0] * self.output_dpi) > self.max_width:
|
|
279
|
+
# Adjust our tick labels.
|
|
280
|
+
x_difference = abs(self.axes.get_xlim()[0] - self.axes.get_xlim()[1])
|
|
281
|
+
if x_difference < 1200:
|
|
282
|
+
self.axes.xaxis.set_major_locator(ticker.MultipleLocator(100))
|
|
283
|
+
self.axes.xaxis.set_minor_locator(ticker.MultipleLocator(50))
|
|
284
|
+
else:
|
|
285
|
+
self.axes.xaxis.set_major_locator(ticker.MultipleLocator(200))
|
|
286
|
+
self.axes.xaxis.set_minor_locator(ticker.MultipleLocator(100))
|
|
287
|
+
|
|
288
|
+
# Update our layout.
|
|
289
|
+
self.figure.tight_layout()
|
|
290
|
+
|
|
291
|
+
class Absorption_graph_maker(Absorption_emission_graph_maker):
|
|
292
|
+
"""
|
|
293
|
+
Class for creating absorption (UV-Vis) spectra.
|
|
294
|
+
"""
|
|
295
|
+
options_name = "absorption_spectrum"
|
|
296
|
+
|
|
297
|
+
class Emission_graph_maker(Absorption_emission_graph_maker):
|
|
298
|
+
"""
|
|
299
|
+
Class for creating emission spectra.
|
|
300
|
+
"""
|
|
301
|
+
options_name = "emission_spectrum"
|
|
302
|
+
|
|
303
|
+
class Frequency_graph_maker(Spectroscopy_graph_maker):
|
|
304
|
+
"""
|
|
305
|
+
A graph for displaying vibrational frequencies.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
def __init__(self, output, graph, *args, **kwargs):
|
|
309
|
+
"""
|
|
310
|
+
Constructor for frequency graphs.
|
|
311
|
+
|
|
312
|
+
:param output: A path to an output file to write to. The extension of this path is used to determine the format of the file (eg, png, jpeg).
|
|
313
|
+
:param graph: An instance of a digichem.result.spectroscopy.Spectroscopy_graph that contains data to plot.
|
|
314
|
+
:param x_limits_method: String controlling how the x axis limits are set. Options are 'auto' for standard auto scaling, showing all plotted peaks. Alternatively, a tuple of (x_min, x_max) which will be used directly as axis limits.
|
|
315
|
+
:param y_limits_method: String controlling how the y axis limits are set. Options are 'auto' for standard auto scaling. Alternatively, a tuple of (y_min, y_max) which will be used directly as axis limits.
|
|
316
|
+
"""
|
|
317
|
+
# Call our parent.
|
|
318
|
+
super().__init__(output, graph, *args, **kwargs)
|
|
319
|
+
|
|
320
|
+
# Axes labels.
|
|
321
|
+
self.x_label = "Frequency /cm$^{-1}$"
|
|
322
|
+
self.y_label = "Intensity /km mol$^{-1}$"
|
|
323
|
+
|
|
324
|
+
# Space to allocate per unit of the y axis.
|
|
325
|
+
self.inch_per_x = 0.00252
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@classmethod
|
|
329
|
+
def from_options(self, output, *, vibrations, options, **kwargs):
|
|
330
|
+
"""
|
|
331
|
+
Constructor that takes a dictionary of config like options.
|
|
332
|
+
"""
|
|
333
|
+
return self(
|
|
334
|
+
output,
|
|
335
|
+
graph = Spectroscopy_graph.from_vibrations(vibrations, options['IR_spectrum']['fwhm'], options['IR_spectrum']['gaussian_resolution'], options['IR_spectrum']['gaussian_cutoff']),
|
|
336
|
+
**{key: value for key, value in options['IR_spectrum'].items() if key not in ["gaussian_cutoff", "gaussian_resolution", "fwhm"]},
|
|
337
|
+
**kwargs
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def auto_x_limits(self):
|
|
341
|
+
"""
|
|
342
|
+
Limit the X axis so all plotted peaks are visible.
|
|
343
|
+
"""
|
|
344
|
+
# We use automatic X scaling, except we don't go past 0, we show at least 3500 cm-1 and our scale is reversed (this is typical for IR).
|
|
345
|
+
self.axes.autoscale(enable = True, axis = 'x')
|
|
346
|
+
self.axes.set_xlim(max(self.axes.get_xlim()[1], 3500), 0)
|
|
347
|
+
|
|
348
|
+
def auto_y_limits(self):
|
|
349
|
+
"""
|
|
350
|
+
Limit the y axis so all plotted peaks are visible.
|
|
351
|
+
"""
|
|
352
|
+
# We use automatic Y scaling, except we don't go past 0 and we are reversed (this is typical for IR).
|
|
353
|
+
self.axes.autoscale(enable = True, axis = 'y')
|
|
354
|
+
self.axes.set_ylim(self.axes.get_ylim()[1], 0)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class NMR_graph_maker_abc(Spectroscopy_graph_maker):
|
|
358
|
+
"""
|
|
359
|
+
ABC for NMR spectra.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
def __init__(self, output, graph, coupling = None, plot_labels = True, **kwargs):
|
|
363
|
+
"""
|
|
364
|
+
:param output: A path to an output file to write to. The extension of this path is used to determine the format of the file (eg, png, jpeg).
|
|
365
|
+
:param graph: An instance of a digichem.result.spectroscopy.Spectroscopy_graph that contains data to plot.
|
|
366
|
+
"""
|
|
367
|
+
# Call our parent.
|
|
368
|
+
super().__init__(output, graph, **kwargs)
|
|
369
|
+
|
|
370
|
+
# A dictionary of dicts. The outer key is the atom group
|
|
371
|
+
self.coupling = coupling if coupling is not None else {}
|
|
372
|
+
|
|
373
|
+
# Axes labels.
|
|
374
|
+
self.x_label = "Chemical Shift /ppm"
|
|
375
|
+
self.y_label = "Arbitrary Units"
|
|
376
|
+
#self.hide_y = True
|
|
377
|
+
self.max_width = 1600
|
|
378
|
+
|
|
379
|
+
# Space to allocate per unit of the y axis.
|
|
380
|
+
self.inch_per_x = 15
|
|
381
|
+
|
|
382
|
+
self.linewidth = 0.5
|
|
383
|
+
self.cumulative_linewidth = 0.5
|
|
384
|
+
self.column_linewidth = 0.5
|
|
385
|
+
|
|
386
|
+
self.should_plot_labels = plot_labels
|
|
387
|
+
|
|
388
|
+
self.x_padding = 0.05
|
|
389
|
+
self.y_padding_percent = 1.5
|
|
390
|
+
|
|
391
|
+
self.annotations = []
|
|
392
|
+
self.focus_label = None
|
|
393
|
+
|
|
394
|
+
def plot(self):
|
|
395
|
+
"""
|
|
396
|
+
Plot the contents of our graph.
|
|
397
|
+
"""
|
|
398
|
+
super().plot()
|
|
399
|
+
if self.should_plot_labels:
|
|
400
|
+
self.plot_labels()
|
|
401
|
+
|
|
402
|
+
def get_label_for_peak(self, atom_group, graph, full = False):
|
|
403
|
+
# Get the median of the x values (the middle point).
|
|
404
|
+
x_coords, y_coords = self.transpose(graph.plot_cumulative_gaussian())
|
|
405
|
+
|
|
406
|
+
x_coord = statistics.median(x_coords)
|
|
407
|
+
y_coord = max(y_coords)
|
|
408
|
+
|
|
409
|
+
# Now filter by a fraction of that amount.
|
|
410
|
+
visible_x_values = [x for x, y in graph.plot_cumulative_gaussian() if y >= (y_coord * 0.95)]
|
|
411
|
+
|
|
412
|
+
# Also get the height of the total curve at this point.
|
|
413
|
+
total_x_coord, total_y_coords = self.transpose(((x,y) for x, y in self.graph.plot_cumulative_gaussian() if x >= min(visible_x_values) and x <= max(visible_x_values)))
|
|
414
|
+
total_max_y = max(total_y_coords)
|
|
415
|
+
|
|
416
|
+
# Get multiplicity of the peak.
|
|
417
|
+
coupling = self.coupling.get(atom_group, {})
|
|
418
|
+
mult = graph.multiplicity(atom_group, coupling)
|
|
419
|
+
mult_string = "".join((multiplicity['symbol'] for multiplicity in mult))
|
|
420
|
+
|
|
421
|
+
if full:
|
|
422
|
+
label = r"$\mathdefault{{{{{}}}_{{{}}}}}$ ({})".format(atom_group.element, atom_group.index, mult_string)
|
|
423
|
+
#label += "\n" + r"$\int$ = {}{}".format(len(atom_group.atoms), atom_group.element.symbol)
|
|
424
|
+
#label += "\n{:.2f} ppm".format(x_coord) + r", $\int$ = {}{}".format(len(atom_group.atoms), atom_group.element.symbol)
|
|
425
|
+
label += "\n{:.2f} ppm".format(x_coord) + r", {}{}".format(len(atom_group.atoms), atom_group.element.symbol)
|
|
426
|
+
|
|
427
|
+
else:
|
|
428
|
+
label = r"$\mathdefault{{{{{}}}_{{{}}}}}$".format(atom_group.element, atom_group.index)
|
|
429
|
+
|
|
430
|
+
return label, x_coord, total_max_y
|
|
431
|
+
#return label, x_coord, y_coord
|
|
432
|
+
|
|
433
|
+
def plot_label_for_peak(self, label, x_coord, y_coord):
|
|
434
|
+
"""
|
|
435
|
+
Plot a label for a given sub-graph.
|
|
436
|
+
"""
|
|
437
|
+
# return self.axes.annotate(
|
|
438
|
+
# "{}".format(label),
|
|
439
|
+
# (x_coord, y_coord),
|
|
440
|
+
# textcoords = "offset pixels",
|
|
441
|
+
# xytext = (0, 8),
|
|
442
|
+
# horizontalalignment = "center",
|
|
443
|
+
# fontsize = 8
|
|
444
|
+
# )
|
|
445
|
+
text = self.axes.text(
|
|
446
|
+
# Pop the label 5% above the peak.
|
|
447
|
+
x_coord, y_coord * 1.1,
|
|
448
|
+
label,
|
|
449
|
+
horizontalalignment = "center",
|
|
450
|
+
fontsize = 10
|
|
451
|
+
)
|
|
452
|
+
text.set_bbox({"facecolor": 'white', "alpha": 0.75})
|
|
453
|
+
return text
|
|
454
|
+
|
|
455
|
+
def plot_labels(self):
|
|
456
|
+
"""
|
|
457
|
+
Plot labels for each peak.
|
|
458
|
+
"""
|
|
459
|
+
self.annotations = []
|
|
460
|
+
for atom_group, graph in self.graph.graphs.items():
|
|
461
|
+
self.annotations.append(self.plot_label_for_peak(*self.get_label_for_peak(atom_group, graph)))
|
|
462
|
+
|
|
463
|
+
def make_graph(self):
|
|
464
|
+
figure = super().make_graph()
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
x, y = self.transpose(self.graph.peaks())
|
|
468
|
+
# Decrease the value of all y (because adjusttext will repel labels away if they are too close)
|
|
469
|
+
y = [val * 0.8 for val in y]
|
|
470
|
+
|
|
471
|
+
#self.annotations = []
|
|
472
|
+
|
|
473
|
+
if len(self.annotations) > 0:
|
|
474
|
+
adjustText.adjust_text(
|
|
475
|
+
self.annotations,
|
|
476
|
+
x = x,
|
|
477
|
+
y = y,
|
|
478
|
+
objects = [self.focus_label] if self.focus_label is not None else None,
|
|
479
|
+
ax = self.axes,
|
|
480
|
+
avoid_self = False,
|
|
481
|
+
expand_axes = False,
|
|
482
|
+
expand = (1.5, 1.3),
|
|
483
|
+
arrowprops = {"arrowstyle": '-', "linewidth": 0.5},
|
|
484
|
+
min_arrow_len = 1,
|
|
485
|
+
# Don't allow moving down on the y axis.
|
|
486
|
+
#only_move = "xy+"
|
|
487
|
+
only_move = "xy",
|
|
488
|
+
#only_move = "y+",
|
|
489
|
+
time_lim = 10
|
|
490
|
+
)
|
|
491
|
+
return figure
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class NMR_graph_maker(NMR_graph_maker_abc):
|
|
495
|
+
"""
|
|
496
|
+
A graph for displaying NMR spectra
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
def __init__(self, output, graph, coupling = None, **kwargs):
|
|
500
|
+
"""
|
|
501
|
+
:param output: A path to an output file to write to. The extension of this path is used to determine the format of the file (eg, png, jpeg).
|
|
502
|
+
:param graph: An instance of a digichem.result.spectroscopy.Spectroscopy_graph that contains data to plot.
|
|
503
|
+
"""
|
|
504
|
+
# Call our parent.
|
|
505
|
+
super().__init__(output, graph, coupling = coupling, **kwargs)
|
|
506
|
+
|
|
507
|
+
self.x_padding = None
|
|
508
|
+
self.x_padding_percent = 0.05
|
|
509
|
+
|
|
510
|
+
# NMR graphs have a range of different units depending on the atom and isotope being studied.
|
|
511
|
+
# For example, 1H NMR typically range from ~10 -> 0
|
|
512
|
+
# 13C typically range from 300-200 -> 0 etc.
|
|
513
|
+
# To account for this, the 'scale' (in pixels) of the x-axis will be adjusted depending on the
|
|
514
|
+
# range of values we have.
|
|
515
|
+
# This is in inches.
|
|
516
|
+
self.target_width = 10
|
|
517
|
+
|
|
518
|
+
# Space to allocate per unit of the y axis.
|
|
519
|
+
self.inch_per_x = None
|
|
520
|
+
|
|
521
|
+
self.linewidth = 0.5
|
|
522
|
+
self.cumulative_linewidth = 0.5
|
|
523
|
+
self.column_linewidth = 0.5
|
|
524
|
+
|
|
525
|
+
@classmethod
|
|
526
|
+
def from_options(self, output, graph, *, options, **kwargs):
|
|
527
|
+
"""
|
|
528
|
+
Constructor that takes a dictionary of config like options.
|
|
529
|
+
"""
|
|
530
|
+
return self(
|
|
531
|
+
output,
|
|
532
|
+
graph,
|
|
533
|
+
enable_rendering = options['nmr']['enable_rendering'],
|
|
534
|
+
plot_bars = options['nmr']['plot_bars'],
|
|
535
|
+
**kwargs
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def get_zoomed(self, output, focus, options, **kwargs):
|
|
539
|
+
"""
|
|
540
|
+
Return a zoomed spectrum (a NMR_graph_zoom_maker object) focusing on a single peak.
|
|
541
|
+
|
|
542
|
+
:param focus: The name of a peak.
|
|
543
|
+
"""
|
|
544
|
+
return NMR_graph_zoom_maker.from_options(output, self.graph, coupling = self.coupling, focus = focus, options = options, **kwargs)
|
|
545
|
+
|
|
546
|
+
def auto_x_limits(self):
|
|
547
|
+
"""
|
|
548
|
+
Limit the X axis so all plotted peaks are visible.
|
|
549
|
+
"""
|
|
550
|
+
# We need to get a list of all peaks that are above our cutoff point.
|
|
551
|
+
# First determine our highest point.
|
|
552
|
+
highest_point = max(self.transpose(self.graph.coordinates)[1])
|
|
553
|
+
|
|
554
|
+
# Now filter by a fraction of that amount.
|
|
555
|
+
visible_x_values = [x for x, y in self.graph.plot_cumulative_gaussian() if y >= (highest_point * self.peak_cutoff)]
|
|
556
|
+
|
|
557
|
+
# Now we can just get our min-max.
|
|
558
|
+
# NMR is typically shown on a relative scale, meaning that negative values are absolutely possible.
|
|
559
|
+
# We always want to make sure that zero is shown however.
|
|
560
|
+
#
|
|
561
|
+
# NMR is also typically shown on a reversed scale.
|
|
562
|
+
|
|
563
|
+
x_padding = (
|
|
564
|
+
max(visible_x_values) - min(visible_x_values)
|
|
565
|
+
) * self.x_padding_percent
|
|
566
|
+
|
|
567
|
+
# If we have no negative shifts, set zero as the end of one scale.
|
|
568
|
+
if min(visible_x_values) < 0:
|
|
569
|
+
minimum = min(visible_x_values) - x_padding
|
|
570
|
+
|
|
571
|
+
else:
|
|
572
|
+
# Don't extend beyond zero if there are no negative values.
|
|
573
|
+
minimum = min(visible_x_values) - x_padding
|
|
574
|
+
#minimum = 0
|
|
575
|
+
|
|
576
|
+
# If we have no positive shifts, set zero as the end of one scale.
|
|
577
|
+
if max(visible_x_values) > 0:
|
|
578
|
+
maximum = max(visible_x_values) + x_padding
|
|
579
|
+
|
|
580
|
+
else:
|
|
581
|
+
maximum = 0
|
|
582
|
+
|
|
583
|
+
# Space to allocate per unit of the y axis.
|
|
584
|
+
self.inch_per_x = self.target_width / (maximum - minimum)
|
|
585
|
+
|
|
586
|
+
return self.axes.set_xlim(maximum, minimum)
|
|
587
|
+
|
|
588
|
+
def auto_y_limits(self):
|
|
589
|
+
"""
|
|
590
|
+
Limit the y axis so all plotted peaks are visible.
|
|
591
|
+
"""
|
|
592
|
+
super().auto_y_limits()
|
|
593
|
+
self.axes.set_ylim(0, self.axes.get_ylim()[1] * self.y_padding_percent)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class NMR_graph_zoom_maker(NMR_graph_maker_abc):
|
|
597
|
+
"""
|
|
598
|
+
A class for generating a zoomed NMR spectrum of a single (split) peak.
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
def __init__(self, output, graph, focus, coupling = None, plot_background_peaks = False, **kwargs):
|
|
602
|
+
"""
|
|
603
|
+
Constructor for frequency graphs.
|
|
604
|
+
|
|
605
|
+
:param output: A path to an output file to write to. The extension of this path is used to determine the format of the file (eg, png, jpeg).
|
|
606
|
+
:param graph: An instance of a digichem.result.spectroscopy.Spectroscopy_graph that contains data to plot.
|
|
607
|
+
:param focus: An atom group to focus on.
|
|
608
|
+
"""
|
|
609
|
+
# Call our parent.
|
|
610
|
+
super().__init__(
|
|
611
|
+
output,
|
|
612
|
+
graph,
|
|
613
|
+
coupling = coupling,
|
|
614
|
+
plot_peaks = True,
|
|
615
|
+
plot_cumulative_peak = True,
|
|
616
|
+
x_limits = 'auto',
|
|
617
|
+
peak_cutoff = 0.02,
|
|
618
|
+
**kwargs)
|
|
619
|
+
|
|
620
|
+
self.plot_background_peaks = plot_background_peaks
|
|
621
|
+
self.focus = focus
|
|
622
|
+
|
|
623
|
+
self.x_padding = None
|
|
624
|
+
self.x_padding_percent = 1.0
|
|
625
|
+
|
|
626
|
+
self.target_width = 3.5
|
|
627
|
+
|
|
628
|
+
# Space to allocate per unit of the y axis.
|
|
629
|
+
self.inch_per_x = None
|
|
630
|
+
|
|
631
|
+
self.cumulative_linewidth = 1
|
|
632
|
+
|
|
633
|
+
if self.focus not in self.graph.graphs:
|
|
634
|
+
raise ValueError("The sub-graph '{}' is not recognised".format(self.focus))
|
|
635
|
+
|
|
636
|
+
@classmethod
|
|
637
|
+
def from_options(self, output, graph, *, options, **kwargs):
|
|
638
|
+
"""
|
|
639
|
+
Constructor that takes a dictionary of config like options.
|
|
640
|
+
"""
|
|
641
|
+
return self(
|
|
642
|
+
output,
|
|
643
|
+
graph,
|
|
644
|
+
enable_rendering = options['nmr']['enable_rendering'],
|
|
645
|
+
plot_bars = options['nmr']['plot_zoom_bars'],
|
|
646
|
+
**kwargs
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
def visible_window(self):
|
|
650
|
+
"""
|
|
651
|
+
Determine the x-axis window that is to be displayed.
|
|
652
|
+
"""
|
|
653
|
+
# Get the graph object corresponding to the peak we are interested in.
|
|
654
|
+
graph = self.graph.graphs[self.focus]
|
|
655
|
+
|
|
656
|
+
# We need to get a list of all peaks that are above our cutoff point.
|
|
657
|
+
# First determine our highest point.
|
|
658
|
+
highest_point = max(self.transpose(graph.coordinates)[1])
|
|
659
|
+
|
|
660
|
+
# Now filter by a fraction of that amount.
|
|
661
|
+
visible_x_values = [x for x, y in graph.plot_cumulative_gaussian() if y >= (highest_point * self.peak_cutoff)]
|
|
662
|
+
|
|
663
|
+
x_padding = (
|
|
664
|
+
max(visible_x_values) -min(visible_x_values)
|
|
665
|
+
) * self.x_padding_percent
|
|
666
|
+
|
|
667
|
+
maximum = max(visible_x_values) + x_padding
|
|
668
|
+
minimum = min(visible_x_values) - x_padding
|
|
669
|
+
|
|
670
|
+
return (minimum, maximum)
|
|
671
|
+
|
|
672
|
+
def auto_x_limits(self):
|
|
673
|
+
"""
|
|
674
|
+
Limit the X axis so all plotted peaks are visible.
|
|
675
|
+
"""
|
|
676
|
+
minimum, maximum = self.visible_window()
|
|
677
|
+
|
|
678
|
+
# Space to allocate per unit of the y axis.
|
|
679
|
+
self.inch_per_x = self.target_width / (maximum - minimum)
|
|
680
|
+
|
|
681
|
+
# Now we can just get our min-max.
|
|
682
|
+
self.axes.set_xlim(
|
|
683
|
+
maximum,
|
|
684
|
+
minimum
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
def auto_y_limits(self):
|
|
688
|
+
"""
|
|
689
|
+
Limit the y axis so all plotted peaks are visible.
|
|
690
|
+
"""
|
|
691
|
+
# Get the graph object corresponding to the peak we are interested in.
|
|
692
|
+
graph = self.graph.graphs[self.focus]
|
|
693
|
+
focus_spectrum = self.transpose(graph.plot_cumulative_gaussian())
|
|
694
|
+
|
|
695
|
+
# Get all peaks.
|
|
696
|
+
minimum, maximum = self.visible_window()
|
|
697
|
+
spectrum = self.transpose([(x,y) for x, y in self.graph.plot_cumulative_gaussian() if x >= minimum and x <= maximum])
|
|
698
|
+
|
|
699
|
+
# # Cut the full spectrum within the limits of our focus graph.
|
|
700
|
+
# # NOTE: This works well, but as the plotted graph is normally wider than
|
|
701
|
+
# # just the values of the peak of interest (because of x_padding), there
|
|
702
|
+
# # may still be peaks in view which are cut off...
|
|
703
|
+
# cut_start = spectrum[0].index(focus_spectrum[0][0])
|
|
704
|
+
# cut_end = spectrum[0].index(focus_spectrum[0][-1])
|
|
705
|
+
#
|
|
706
|
+
# cut_spectrum = (spectrum[0][cut_start:cut_end], spectrum[1][cut_start:cut_end])
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
highest_point = max(spectrum[1])
|
|
711
|
+
|
|
712
|
+
# Clamp to 0 -> pos.
|
|
713
|
+
self.axes.set_ylim(0, highest_point * 1.3)
|
|
714
|
+
|
|
715
|
+
def plot_lines(self):
|
|
716
|
+
"""
|
|
717
|
+
Plot x and y values as individual lines on the graph we are building.
|
|
718
|
+
"""
|
|
719
|
+
if self.plot_background_peaks:
|
|
720
|
+
for graph_name, graph in self.graph.graphs.items():
|
|
721
|
+
if graph_name != self.focus:
|
|
722
|
+
plot = graph.plot_cumulative_gaussian()
|
|
723
|
+
data = self.transpose(plot)
|
|
724
|
+
self.axes.plot(*data, 'C0--', linewidth = self.linewidth)
|
|
725
|
+
|
|
726
|
+
# Plot our focus last (so it appears on top).
|
|
727
|
+
plot = self.graph.graphs[self.focus].plot_cumulative_gaussian()
|
|
728
|
+
data = self.transpose(plot)
|
|
729
|
+
self.axes.plot(*data, 'r-', linewidth = self.cumulative_linewidth)
|
|
730
|
+
|
|
731
|
+
def plot_columns(self):
|
|
732
|
+
"""
|
|
733
|
+
Plot vertical columns for each plot on the graph we are building.
|
|
734
|
+
"""
|
|
735
|
+
for atom_group, graph in self.graph.graphs.items():
|
|
736
|
+
if atom_group != self.focus:
|
|
737
|
+
columns = [[(x, 0) , (x, y)] for x, y in graph.coordinates]
|
|
738
|
+
colors = (0,0,0)
|
|
739
|
+
self.axes.add_collection(matplotlib.collections.LineCollection(columns, linewidths = self.column_linewidth, colors = colors))
|
|
740
|
+
|
|
741
|
+
graph = self.graph.graphs[self.focus]
|
|
742
|
+
columns = [[(x, 0) , (x, y)] for x, y in graph.coordinates]
|
|
743
|
+
colors = (1,0,0)
|
|
744
|
+
self.axes.add_collection(matplotlib.collections.LineCollection(columns, linewidths = self.column_linewidth, colors = colors))
|
|
745
|
+
|
|
746
|
+
def plot_labels(self):
|
|
747
|
+
"""
|
|
748
|
+
Plot labels for each peak.
|
|
749
|
+
"""
|
|
750
|
+
self.annotations = []
|
|
751
|
+
minimum, maximum = self.visible_window()
|
|
752
|
+
|
|
753
|
+
for atom_group, graph in self.graph.graphs.items():
|
|
754
|
+
if atom_group != self.focus:
|
|
755
|
+
# Only plot the label if the peak is within our visible window.
|
|
756
|
+
x_coords, y_coords = self.transpose(graph.plot_cumulative_gaussian())
|
|
757
|
+
x_coord = statistics.median(x_coords)
|
|
758
|
+
if x_coord > minimum and x_coord < maximum:
|
|
759
|
+
self.annotations.append(
|
|
760
|
+
self.plot_label_for_peak(*self.get_label_for_peak(atom_group, graph))
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Plot a more complete label for our focus peak.
|
|
764
|
+
label, x_coord, y_coord = self.get_label_for_peak(self.focus, self.graph.graphs[self.focus], full = True)
|
|
765
|
+
|
|
766
|
+
# Get couplings for our main atom group.
|
|
767
|
+
coupling = self.coupling.get(self.focus, {})
|
|
768
|
+
|
|
769
|
+
# Get the multiplicities of our peak.
|
|
770
|
+
mult = self.graph.graphs[self.focus].multiplicity(self.focus, coupling)
|
|
771
|
+
couplings = unpack_coupling(coupling)
|
|
772
|
+
|
|
773
|
+
# Add coupling info if we have it.
|
|
774
|
+
# couplings = self.coupling.get(self.focus, {})
|
|
775
|
+
# couplings = {
|
|
776
|
+
# (coupling_group, coupling_isotope): isotope_coupling for coupling_group, atom_dict in couplings.items() for coupling_isotope, isotope_coupling in atom_dict.items()
|
|
777
|
+
# if isotope_coupling.groups[isotope_coupling.other(atom_group)].element[isotope_coupling.isotopes[isotope_coupling.other(atom_group)]].abundance / 100 > satellite_threshold
|
|
778
|
+
# }
|
|
779
|
+
# # Sort couplings.
|
|
780
|
+
# couplings = dict(sorted(couplings.items(), key = lambda coupling: abs(coupling[1].total), reverse = True))
|
|
781
|
+
|
|
782
|
+
# Add coupling info.
|
|
783
|
+
if len(couplings) > 0 and mult[0]["number"] != 1:
|
|
784
|
+
# Only show couplings for peaks we can actually distinguish.
|
|
785
|
+
for (coupling_group, coupling_isotope), coupling in list(couplings.items())[:len(mult)]:
|
|
786
|
+
#for (coupling_group, coupling_isotope), coupling in [isotope_coupling for atom_dict in couplings.values() for isotope_coupling in atom_dict.items()][:len(mult)]:
|
|
787
|
+
label += "\n" + r"J = {:.2f} Hz ($\mathdefault{{^{{{}}}{}_{{{}}}}}$, {}{})".format(
|
|
788
|
+
coupling.total,
|
|
789
|
+
coupling_isotope,
|
|
790
|
+
coupling_group.element,
|
|
791
|
+
coupling_group.index,
|
|
792
|
+
#len(coupling_group.atoms),
|
|
793
|
+
coupling.num_coupled_atoms(self.focus),
|
|
794
|
+
coupling_group.element
|
|
795
|
+
)
|
|
796
|
+
#self.focus_label = self.plot_label_for_peak(label, x_coord, y_coord)
|
|
797
|
+
self.annotations.append(self.plot_label_for_peak(label, x_coord, y_coord))
|