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/nmr.py ADDED
@@ -0,0 +1,1086 @@
1
+ import numpy
2
+ from itertools import filterfalse
3
+ import periodictable
4
+ from fractions import Fraction
5
+ import re
6
+ import statistics
7
+ import math
8
+
9
+ from digichem.misc.base import regular_range, powerset
10
+ from digichem.exception.base import Result_unavailable_error
11
+ from digichem.result.base import Result_object, Result_container, Floatable_mixin
12
+ import digichem.log
13
+
14
+ # Hidden.
15
+ #from digichem.result.spectroscopy import Combined_graph
16
+
17
+
18
+ # TODO: NMR tensors are currently not re-orientated according to the alignment method used.
19
+ # This needs implementing.
20
+ class NMR_spectrometer(Result_object):
21
+ """
22
+ A class for generating NMR spectra on-demand.
23
+ """
24
+
25
+ def __init__(self, nmr_results, frequency = 300, fwhm = 0.001, resolution = 0.001, cutoff = 0.01, coupling_filter = 0.1, pre_merge = 0.01, post_merge = None, isotope_options = None):
26
+ """
27
+ Constructor for NMR_spectrometer.
28
+
29
+ :param nmr_results: A list of NMR_group result objects.
30
+ :param frequency: The frequency of the simulated spectrometer.
31
+ :param fwhm: The full-width at half-maximum of the simulated peaks (in ppm).
32
+ :param resolution: The resolution/step-size of the plotted peaks (in ppm). Decreasing this value may increase computational time.
33
+ :param reference: An optional reference isotropic value to correct this shielding by.
34
+ """
35
+ self.nmr_results = nmr_results
36
+ self.frequency = frequency
37
+ self.pre_merge = pre_merge
38
+ self.post_merge = post_merge
39
+ self.fwhm = fwhm
40
+ self.gaussian_resolution = resolution
41
+ self.gaussian_cutoff = cutoff
42
+ self.coupling_filter = coupling_filter
43
+ self._isotope_options = isotope_options if isotope_options is not None else {}
44
+
45
+ def isotope_options(self, element, isotope):
46
+ options = {
47
+ "frequency": self.frequency,
48
+ "pre_merge": self.pre_merge,
49
+ "post_merge": self.post_merge,
50
+ "fwhm": self.fwhm,
51
+ "gaussian_resolution": self.gaussian_resolution,
52
+ "coupling_filter": self.coupling_filter,
53
+ "gaussian_cutoff": self.gaussian_cutoff,
54
+ }
55
+ options.update(self._isotope_options.get("{}{}".format(isotope, periodictable.elements[element].symbol), {}))
56
+ return options
57
+
58
+ @classmethod
59
+ def from_options(self, nmr_results, *, options, **kwargs):
60
+ """
61
+ Constructor that takes a dictionary of config like options.
62
+ """
63
+ return self(
64
+ nmr_results,
65
+ frequency = options['nmr']['frequency'],
66
+ fwhm = options['nmr']['fwhm'],
67
+ resolution = options['nmr']['gaussian_resolution'],
68
+ cutoff = options['nmr']['gaussian_cutoff'],
69
+ coupling_filter = options['nmr']['coupling_filter'],
70
+ pre_merge = options['nmr']['pre_merge'],
71
+ post_merge = options['nmr']['post_merge'],
72
+ isotope_options = options['nmr']['isotopes'],
73
+ **kwargs
74
+ )
75
+
76
+ @property
77
+ def available(self):
78
+ """
79
+ Determine which nuclei NMR can be simulated for.
80
+
81
+ :return: A set of the available nuclei, each as a tuple of (proton_number, isotope). Note that the special value of 0 is used to indicate isotope-independent data is available. Eg, (6, 0) would indicate isotope-independent (no coupling) carbon NMR.
82
+ """
83
+ nuclei = set()
84
+
85
+ for nmr_result in self.nmr_results.values():
86
+ # Add the generic (non-isotope specific) element to our list of supported types.
87
+ nuclei.add((nmr_result.group.element.number, 0, ()))
88
+
89
+ couplings = {}
90
+
91
+ # Also consider specific isotope coupling.
92
+ for coupling in nmr_result.couplings:
93
+ main_index = coupling.groups.index(nmr_result.group)
94
+ second_index = abs(1 - main_index)
95
+
96
+ if coupling.isotopes[main_index] not in couplings:
97
+ couplings[coupling.isotopes[main_index]] = set()
98
+
99
+ couplings[coupling.isotopes[main_index]].add( (coupling.groups[second_index].element.number, coupling.isotopes[second_index]) )
100
+
101
+ for isotope, isotope_couplings in couplings.items():
102
+ for combination in powerset(sorted(isotope_couplings)):
103
+ nuclei.add((nmr_result.group.element.number, isotope, tuple(combination)))
104
+
105
+ return {
106
+ self.make_shortcode(element, isotope, decoupling):
107
+ {
108
+ "element": element,
109
+ "isotope": isotope,
110
+ #"decoupling": [list(decouple) for decouple in decoupling],
111
+ "decoupling": list(decoupling)
112
+ } for element, isotope, decoupling in sorted(nuclei, key = lambda nmr: (nmr[0], nmr[1]))
113
+ }
114
+
115
+ @classmethod
116
+ def parse_shortcode(self, code):
117
+ """
118
+ Parse an NMR shortcode.
119
+
120
+ Each shortcode specifies an element and one of its isotopes to investigate, optionally followed by a list of isotopes to decouple (not show coupling for).
121
+ For example:
122
+ 13C{1H} - Carbon-13 spectrum with protons decoupled.
123
+ 1H{1H,13C} - Hydrogen-1 spectrum with protons and 13C decoupled.
124
+ *C or 0C - Carbon NMR spectrum with no coupling considered.
125
+ """
126
+ match = re.search(r'(\d+|\*)([A-z][A-z]?){?((\d+[A-z][A-z]?,?)*)}?', code)
127
+
128
+ if match is None:
129
+ raise ValueError("Unable to process NMR shortcode '{}'".format(code))
130
+
131
+ match_groups = match.groups()
132
+ isotope = int(match_groups[0]) if match_groups[0] != "*" else 0
133
+ element = getattr(periodictable, match_groups[1]).number
134
+
135
+ decoupling = [tuple(re.search("(\d+)([A-z][A-z]?)", decouple).groups()) for decouple in match_groups[2].split(",") if re.search("(\d+)([A-z][A-z]?)", decouple) is not None]
136
+ decoupling = sorted([(getattr(periodictable, ele).number, int(iso)) for iso, ele in decoupling])
137
+
138
+ return element, isotope, decoupling
139
+
140
+ @classmethod
141
+ def make_shortcode(self, element, isotope, decoupling):
142
+ """
143
+ Create an NMR shortcode from a given NMR experiment.
144
+ """
145
+ return "{}{}".format(isotope, periodictable.elements[element].symbol) + (
146
+ "{{{}}}".format(",".join(["{}{}".format(decouple[1], periodictable.elements[decouple[0]]) for decouple in decoupling]))
147
+ if len(decoupling) > 0 else "")
148
+
149
+
150
+ def __contains__(self, item):
151
+ """
152
+ Can this spectrometer generate a given spectrum.
153
+ """
154
+ element, isotope, decoupling = self.parse_shortcode(item)
155
+
156
+ #for available_element, available_isotope, available_decoupling in self.available.values():
157
+ for available in self.available.values():
158
+ if element == available['element'] and isotope == available['isotope'] and sorted(decoupling) == sorted(available['decoupling']):
159
+ return True
160
+
161
+ return False
162
+
163
+ def __getitem__(self, item):
164
+ """
165
+ Generate a spectrum for a given shortcode.
166
+ """
167
+ # We return a lambda function here because of the slightly strange dumping mechanism.
168
+ # This object is setup as a generate_for_dump() target. generate_for_dump() returns a
169
+ # dict of callables. We don't actually require this behaviour, but we still need to
170
+ # match the interface.
171
+ return lambda digichem_options: self.spectrum(*self.parse_shortcode(item))
172
+
173
+ def generate_for_dump(self):
174
+ """
175
+ Method used to get a dictionary used to generate on-demand values for dumping.
176
+
177
+ This functionality is useful for hiding expense properties from the normal dump process, while still exposing them when specifically requested.
178
+
179
+ 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.
180
+ """
181
+ return self
182
+
183
+ def dump(self, digichem_options):
184
+ # Return a list of possible spectra we can generate.
185
+ return {
186
+ "codes": list(self.available.keys()),
187
+ #"available": list(self.available.values()),
188
+ }
189
+
190
+ def get_graph(self, element, isotope, decoupling = None):
191
+ """
192
+ Return a result.spectroscopy.Combined_graph object that can be used to generate a spectrum (with Gaussian broadened peaks) for a given experiment.
193
+
194
+ If you are only interested in the spectrum itself (not the machinery that generates it), try spectrum().
195
+ """
196
+ from digichem.result.spectroscopy import Combined_graph
197
+
198
+
199
+ decoupling = decoupling if decoupling is not None else []
200
+
201
+ # First, simulate vertical peaks.
202
+ # Each peaks is grouped by the atom-group that causes it.
203
+ # This allows us to plot merged peaks separately, for example if two atoms result in overlapping chemical shift.
204
+ grouped_peaks = self.simulate(element, isotope, decoupling)
205
+
206
+ # Get options specific to the isotope we're looking at.
207
+ isotope_options = self.isotope_options(element, isotope)
208
+
209
+ # The total spectrum takes all simulated peaks.
210
+ # These are grouped by atom_group, flatten this list before passing to spectroscopy.
211
+ graph = Combined_graph.from_nmr(grouped_peaks, isotope_options['fwhm'], isotope_options['gaussian_resolution'], isotope_options['gaussian_cutoff'])
212
+
213
+ return graph
214
+
215
+ def spectrum(self, element, isotope, decoupling = None):
216
+ """
217
+ Simulate a full NMR spectrum.
218
+
219
+ :param element: The element to simulate (as a proton number).
220
+ :param isotope: The isotope of the element to simulate.
221
+ :param decoupling: A list of elements to 'decouple' (not consider couplings to). Each element should be specified as a tuple of (proton_number, isotope).
222
+ """
223
+ # Get options specific to the isotope we're looking at.
224
+ isotope_options = self.isotope_options(element, isotope)
225
+
226
+ graph = self.get_graph(element, isotope, decoupling)
227
+
228
+ digichem.log.get_logger().info("Simulating broadened NMR peaks for {}{} at {} MHz with {} decoupling".format(
229
+ isotope,
230
+ periodictable.elements[element].symbol,
231
+ isotope_options['frequency'],
232
+ ",".join(["{}{}".format(decouple_iso, periodictable.elements[decouple_ele].symbol) for decouple_ele, decouple_iso in decoupling] if len(decoupling) != 0 else ["no"])
233
+ ))
234
+
235
+ values = {
236
+ "values": [
237
+ {"x":{"value": float(x), "units": "ppm"}, "y": {"value": float(y), "units": "arb"}}
238
+ for x,y in graph.plot_cumulative_gaussian()
239
+ ],
240
+ "groups": {
241
+ atom_group.label: [
242
+ {"x":{"value": float(x), "units": "ppm"}, "y": {"value": float(y), "units": "arb"}}
243
+ for x,y in group_spectrum.plot_cumulative_gaussian()
244
+ ]
245
+ for atom_group, group_spectrum in graph.graphs.items()
246
+ }
247
+ }
248
+ digichem.log.get_logger().info("Done simulating NMR spectrum")
249
+ return values
250
+
251
+ def coupling(self, element, isotope, decoupling = None):
252
+ """
253
+ Return couplings for a given experiment.
254
+
255
+ Be aware that small couplings may be filtered out (not returned) depending on the value of coupling_filter.
256
+
257
+ :returns: The relevant couplings, as a dictionary of dictionary of dicts. The keys of the two outer dict correspond to the atom groups this coupling is between. The inner dict is a coupling dict, containing 'total', 'isotopes' and 'groups'.
258
+ """
259
+ isotope_options = self.isotope_options(element, isotope)
260
+ outer_coupling = {}
261
+ for nmr_result in self.nmr_results.values():
262
+ if nmr_result.group.element.number != element:
263
+ # Wrong element, skip.
264
+ continue
265
+
266
+ # Matches our element.
267
+ inner_coupling = {}
268
+ for coupling in nmr_result.couplings:
269
+ # Check the main isotope matches.
270
+ main_index = coupling.groups.index(nmr_result.group)
271
+ second_index = 1 if main_index == 0 else 0
272
+
273
+ # Only include this coupling if it involves our isotope,
274
+ # and if it's value is above our filter.
275
+ if coupling.isotopes[main_index] == isotope and abs(coupling.total) > isotope_options['coupling_filter']:
276
+ no_couple = False
277
+ # Check we haven't been asked to de-couple this coupling.
278
+ for decouple_element, decouple_isotope in decoupling:
279
+ if coupling.groups[second_index].element.number == decouple_element \
280
+ and coupling.isotopes[second_index] == decouple_isotope:
281
+ # This coupling is good.
282
+ no_couple = True
283
+ break
284
+
285
+ if not no_couple:
286
+ #inner_coupling[(coupling.groups[second_index], coupling.isotopes[second_index])] = coupling
287
+
288
+ # Create a dictionary for this foreign atom.
289
+ if coupling.groups[second_index] not in inner_coupling:
290
+ inner_coupling[coupling.groups[second_index]] = {}
291
+
292
+ # Add another for the isotope of this foreign atom.
293
+ inner_coupling[coupling.groups[second_index]][coupling.isotopes[second_index]] = coupling
294
+
295
+ outer_coupling[nmr_result.group] = inner_coupling
296
+
297
+ return outer_coupling
298
+
299
+ def split_peaks(self, nmr_result, coupling, peaks, isotope_options):
300
+ """
301
+ Split a list of peaks according to a given coupling.
302
+
303
+ :param nmr_result: The NMR result which gives rise to the peak(s) being split.
304
+ :param coupling: The coupling that is being used to cause the splitting.
305
+ :param peaks: An existing list of peaks to split.
306
+ :param isotope_options: Isotope options for this atom.
307
+ """
308
+ main_index = coupling.groups.index(nmr_result.group)
309
+ second_index = 1 if main_index == 0 else 0
310
+
311
+ # Each atom in the group we are coupling to.
312
+ for atom in range(coupling.num_coupled_atoms(nmr_result.group)):
313
+ new_peaks = {}
314
+ for old_peak in peaks.values():
315
+ # Calculate the shift of the new peaks (in ppm).
316
+ coupling_constant = coupling.total / isotope_options['frequency']
317
+
318
+ # If we're merging peaks, round the value appropriately.
319
+ if isotope_options['pre_merge']:
320
+ coupling_constant = round(coupling_constant / isotope_options['pre_merge']) * isotope_options['pre_merge']
321
+
322
+ # Bafflingly, calling 'neutron' here is necessary to make nuclear_spin available.
323
+ ele = getattr(periodictable, coupling.groups[second_index].element.symbol)
324
+ iso = ele[coupling.isotopes[second_index]]
325
+ iso.neutron
326
+ spin = float(Fraction(iso.nuclear_spin))
327
+ #abundance = iso.abundance /100
328
+ num_peaks = 2 * spin +1
329
+
330
+ # Add the new peaks to the shifts originating from coupling between these two groups.
331
+ for new_peak in ([sub_peak, old_peak[1] / num_peaks] for sub_peak in regular_range(old_peak[0], num_peaks, coupling_constant)):
332
+ if new_peak[0] in new_peaks:
333
+ new_peaks[new_peak[0]][1] += new_peak[1]
334
+
335
+ else:
336
+ new_peaks[new_peak[0]] = new_peak
337
+
338
+ peaks = new_peaks
339
+
340
+ return peaks
341
+
342
+ def simulate(self, element, isotope, decoupling = None):
343
+ """
344
+ Simulate vertical NMR peaks.
345
+
346
+ :param element: The element to simulate (as a proton number).
347
+ :param isotope: The isotope of the element to simulate.
348
+ :param decoupling: A list of elements to 'decouple' (not consider couplings to). Each element should be specified as a tuple of (proton_number, isotope).
349
+ """
350
+ element = int(element)
351
+ isotope = int(isotope)
352
+ decoupling = [(int(decouple_ele), int(decouple_iso)) for decouple_ele, decouple_iso in decoupling] if decoupling is not None else None
353
+ isotope_options = self.isotope_options(element, isotope)
354
+
355
+ group_peaks = {}
356
+
357
+ digichem.log.get_logger().info("Simulating NMR peaks for {}{} at {} MHz with {} decoupling".format(
358
+ isotope,
359
+ periodictable.elements[element].symbol,
360
+ isotope_options['frequency'],
361
+ ",".join(["{}{}".format(decouple_iso, periodictable.elements[decouple_ele].symbol) for decouple_ele, decouple_iso in decoupling] if len(decoupling) != 0 else ["no"])
362
+ ))
363
+
364
+ # Get relevant couplings.
365
+ all_coupling = self.coupling(element, isotope, decoupling)
366
+
367
+ for nmr_result in self.nmr_results.values():
368
+ if nmr_result.group.element.number != element:
369
+ # Wrong element, skip.
370
+ continue
371
+
372
+ # Matches our element.
373
+
374
+ group_coupling = all_coupling.get(nmr_result.group, {})
375
+
376
+ # Make some peaks.
377
+ # Start with a single shift peak.
378
+ # 0: chemical shift, 1: intensity
379
+ #
380
+ # If we are merging to a nearest point, do so now (to preserve symmetry).
381
+ if isotope_options['pre_merge']:
382
+ # FYI: Round breaks ties by rounding to the nearest even number (banker's rounding), not by rounding upwards.
383
+ initial_shielding = round(nmr_result.shielding / isotope_options['pre_merge']) * isotope_options['pre_merge']
384
+
385
+ else:
386
+ initial_shielding = nmr_result.shielding
387
+
388
+ peaks = {initial_shielding: [initial_shielding, len(nmr_result.group.atoms)]}
389
+
390
+ # Now split it by each coupling.
391
+ for foreign_atom_group, isotopes in group_coupling.items():
392
+ # Each atom-group that we're going to couple to may have couplings for a number of isotopes.
393
+ # Additionally, any percentage abundance not taken up by the isotopes for which coupling is available
394
+ # will result in an unsplit peak.
395
+ total_abundance = sum([foreign_atom_group.element[isotope].abundance for isotope in isotopes.keys()])
396
+
397
+ new_peaks = {}
398
+
399
+ for isotope, coupling in isotopes.items():
400
+ # Decrease the apparent peak intensity by the natural abundance.
401
+
402
+ abundance = foreign_atom_group.element[isotope].abundance / 100
403
+ isotope_peaks = {peak[0]: (peak[0], peak[1] * abundance) for peak in peaks.values()}
404
+
405
+ for new_peak in self.split_peaks(nmr_result, coupling, isotope_peaks, isotope_options).values():
406
+ if new_peak[0] in new_peaks:
407
+ new_peaks[new_peak[0]] = (new_peak[0], new_peaks[new_peak[0]][1] + new_peak[1])
408
+ else:
409
+ new_peaks[new_peak[0]] = new_peak
410
+
411
+ # After splitting by all isotopes, if we have any abundance not accounted for,
412
+ # we'll add back the original unsplit peaks (with their intensity decreased by the remaining
413
+ # abundance).
414
+ if 100 - total_abundance > 0.0:
415
+ # The starting height of any remaining unsplit peaks.
416
+ abundance = (100 - total_abundance) / 100
417
+
418
+ for new_peak in ((peak[0], peak[1] * abundance) for peak in peaks.values()):
419
+ if new_peak[0] in new_peaks:
420
+ new_peaks[new_peak[0]] = (new_peak[0], new_peaks[new_peak[0]][1] + new_peak[1])
421
+
422
+ else:
423
+ new_peaks[new_peak[0]] = new_peak
424
+
425
+ peaks = new_peaks
426
+
427
+ peaks = list(peaks.values())
428
+ peaks.sort(key = lambda peak: peak[0])
429
+
430
+ # If we've been asked to, merge similar peaks.
431
+ # We do this last so as to not carry forward rounding and averaging errors.
432
+ if isotope_options['post_merge']:
433
+ # Each generated peak will be aligned to a 'grid', the spacing of which is
434
+ # given by post_merge.
435
+ # Start by generating our grid.
436
+ # We want to make sure there is a grid point exactly on our mid-point, this should preserve symmetry.
437
+ median = statistics.median((peak[0] for peak in peaks))
438
+ start = median - math.ceil((median - peaks[0][0]) / isotope_options['post_merge']) * isotope_options['post_merge']
439
+ stop = median + math.ceil((peaks[-1][0] - median) / isotope_options['post_merge']) * isotope_options['post_merge']
440
+ steps = round(((stop - start) / isotope_options['post_merge'])) +1
441
+ new_shifts = numpy.linspace(start, stop, steps)
442
+
443
+ new_peaks = {}
444
+ new_shift_index = 0
445
+
446
+ for peak in peaks:
447
+ # Find where this peak best aligns to our grid.
448
+ # Because we are going through in order, we don't need to start from the beginning of our new peaks.
449
+ for index, new_shift in enumerate(new_shifts[new_shift_index:]):
450
+ if peak[0] < (new_shift + isotope_options['post_merge'] /2):
451
+ if new_shift not in new_peaks:
452
+ # New peaks at this shift.
453
+ new_peaks[new_shift] = [new_shift, peak[1]]
454
+
455
+ else:
456
+ # Existing peak here, add to intensity.
457
+ new_peaks[new_shift][1] += peak[1]
458
+
459
+ # Update our index.
460
+ new_shift_index = index
461
+
462
+ break
463
+
464
+ peaks = list(new_peaks.values())
465
+
466
+ # Done for this group of atoms.
467
+ group_peaks[nmr_result.group] = peaks
468
+
469
+ digichem.log.get_logger().info("Done simulating NMR peaks")
470
+
471
+ return group_peaks
472
+
473
+
474
+ class NMR_list(Result_container):
475
+ """
476
+ A container to hold a list of NMR results.
477
+
478
+ For practical applications, see the output of the group() method or the spectrometer attribute.
479
+ """
480
+
481
+ def __init__(self, *args, atoms, options, **kwargs):
482
+ """
483
+ :param *args: A list of NMR objects.
484
+ :param atoms: An Atom_list object.
485
+ :param options: A digichem options object.
486
+ """
487
+ super().__init__(*args, **kwargs)
488
+ self.atoms = atoms
489
+ self.options = options
490
+ self.groups = self.group()
491
+ self.spectrometer = NMR_spectrometer.from_options(self.groups, options = options)
492
+
493
+ @classmethod
494
+ def from_parser(self, parser):
495
+ return self(NMR.list_from_parser(parser), atoms = parser.results.atoms, options = parser.options)
496
+
497
+ def find(self, criteria = None, *, label = None, index = None):
498
+ return self.search(criteria = criteria, label = label, index = index, allow_empty = False)[0]
499
+
500
+ def search(self, criteria = None, *, label = None, index = None, allow_empty = True):
501
+ """
502
+ """
503
+ if label is None and index is None and criteria is None:
504
+ raise ValueError("One of 'criteria', 'label' or 'index' must be given")
505
+
506
+ if criteria is not None:
507
+ if str(criteria).isdigit():
508
+ index = int(criteria)
509
+
510
+ else:
511
+ label = criteria
512
+
513
+ # Now get our filter func.
514
+ if label is not None:
515
+ filter_func = lambda nmr: nmr.atom.label != label
516
+
517
+ elif index is not None:
518
+ filter_func = lambda nmr: nmr.atom.index != index
519
+
520
+ # Now search.
521
+ found = type(self)(filterfalse(filter_func, self), atoms = self.atoms, options = self.options)
522
+
523
+ if not allow_empty and len(found) == 0:
524
+ if label is not None:
525
+ criteria_string = "label = '{}'".format(label)
526
+
527
+ elif index is not None:
528
+ criteria_string = "index = '{}'".format(index)
529
+
530
+ raise Result_unavailable_error("NMR", "could not find NMR data for atom '{}'".format(criteria_string))
531
+
532
+ return found
533
+
534
+ def group(self, no_self_coupling = True):
535
+ """
536
+ """
537
+ # First, decide which atoms are actually equivalent.
538
+ # We can do this by comparing canonical SMILES groupings.
539
+ atom_groups = self.atoms.groups
540
+
541
+ nmr_groups = {}
542
+ # Next, assemble group objects.
543
+ for group_id, atom_group in atom_groups.items():
544
+ nmr_results = [nmr_result for nmr_result in self if nmr_result.atom in atom_group.atoms]
545
+
546
+ if len(nmr_results) == 0:
547
+ continue
548
+
549
+ # Shieldings.
550
+ shieldings = [nmr_result.shielding for nmr_result in nmr_results]
551
+ # Only keep couplings in which at least one of the two atoms is not in this group (discard self coupling)
552
+ couplings = [coupling for nmr_result in nmr_results for coupling in nmr_result.couplings if not no_self_coupling or len(set(atom_group.atoms).intersection(coupling.atoms)) != 2]
553
+
554
+ nmr_groups[group_id] = {"group": atom_group, "shieldings": shieldings, "couplings": couplings}
555
+
556
+ # Now everything is assembled into groups, re-calculate couplings based on groups only.
557
+ # We need to do this after initial group assembly in order to discard self coupling.
558
+ # Get unique couplings (so we don't consider any twice).
559
+ group_couplings = {}
560
+ unique_couplings = {(coupling.atoms, coupling.isotopes): coupling for group in nmr_groups.values() for coupling in group['couplings']}.values()
561
+ for coupling in unique_couplings:
562
+ # Find the group numbers that correspond to the two atoms in the coupling.
563
+ coupling_groups = tuple([atom_group.id for atom_group in atom_groups.values() if atom in atom_group.atoms][0] for atom in coupling.atoms)
564
+
565
+ isotopes = coupling.isotopes
566
+
567
+ # Append the isotropic coupling constant to the group.
568
+ if coupling_groups not in group_couplings:
569
+ group_couplings[coupling_groups] = {}
570
+
571
+ if isotopes not in group_couplings[coupling_groups]:
572
+ group_couplings[coupling_groups][isotopes] = []
573
+
574
+ group_couplings[coupling_groups][isotopes].append(coupling)
575
+
576
+ # Average each 'equivalent' coupling.
577
+ group_couplings = {
578
+ group_key: {
579
+ isotope_key: NMR_group_spin_coupling(
580
+ groups = [atom_groups[group_sub_key] for group_sub_key in group_key],
581
+ isotopes = isotope_key,
582
+ couplings = isotope_couplings
583
+ ) for isotope_key, isotope_couplings in isotopes.items()}
584
+ for group_key, isotopes in group_couplings.items()
585
+ }
586
+
587
+ # Assemble the final group objects.
588
+ nmr_object_groups = {}
589
+ for group_id, raw_group in nmr_groups.items():
590
+ # Get appropriate couplings.
591
+
592
+ coupling = [
593
+ isotope_coupling
594
+ for group_key, group_coupling in group_couplings.items()
595
+ for isotope_coupling in group_coupling.values()
596
+ if group_id in group_key
597
+ ]
598
+ nmr_object_groups[raw_group['group']] = (NMR_group(raw_group['group'], raw_group['shieldings'], coupling))
599
+
600
+ return nmr_object_groups
601
+
602
+ def dump(self, digichem_options):
603
+ """
604
+ Dump this list of NMR results to a list of primitive types.
605
+ """
606
+ grouping = self.groups
607
+ dump_dict = {
608
+ "values": super().dump(digichem_options),
609
+ "groups": {group_id.label: group.dump(digichem_options) for group_id, group in grouping.items()},
610
+ }
611
+ return dump_dict
612
+
613
+ @classmethod
614
+ def from_dump(self, data, result_set, options):
615
+ """
616
+ Get an instance of this class from its dumped representation.
617
+
618
+ :param data: The data to parse.
619
+ :param result_set: The partially constructed result set which is being populated.
620
+ """
621
+ return self(NMR.list_from_dump(data['values'], result_set, options), atoms = result_set.atoms, options = options)
622
+
623
+ def generate_for_dump(self):
624
+ """
625
+ Method used to get a dictionary used to generate on-demand values for dumping.
626
+
627
+ This functionality is useful for hiding expense properties from the normal dump process, while still exposing them when specifically requested.
628
+
629
+ 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.
630
+ """
631
+ return {
632
+ "spectrum": lambda digichem_options: self.spectrometer
633
+ }
634
+
635
+
636
+ class NMR_group(Result_object, Floatable_mixin):
637
+ """
638
+ A result object containing all the NMR related data for a group of equivalent nuclei.
639
+ """
640
+
641
+ def __init__(self, group, shieldings, couplings):
642
+ self.group = group
643
+ self.shieldings = shieldings
644
+ self.couplings = couplings
645
+
646
+ # Calculate average shieldings and couplings.
647
+ self.shielding = float(sum([shielding.isotropic("total") for shielding in shieldings]) / len(shieldings))
648
+
649
+ def __float__(self):
650
+ return float(self.shielding)
651
+
652
+ def dump(self, digichem_options):
653
+ """
654
+ Get a representation of this result object in primitive format.
655
+ """
656
+ return {
657
+ "group": self.group.label,
658
+ "atoms": [atom.label for atom in self.group.atoms],
659
+ "shielding": {
660
+ "units": "ppm",
661
+ "value": self.shielding
662
+ },
663
+ #"couplings": [{"groups": [group.label for group in coupling['groups']], "isotopes": list(coupling["isotopes"]), "total": coupling["total"]} for coupling in self.couplings],
664
+ "couplings": [coupling.dump(digichem_options) for coupling in self.couplings]
665
+ }
666
+
667
+ class NMR_group_spin_coupling(Result_object):
668
+ """
669
+ A result object containing the average coupling between two different groups of nuclei.
670
+ """
671
+
672
+ def __init__(self, groups, isotopes, couplings):
673
+ """
674
+ :param groups: The two atom groups that this coupling is between.
675
+ :param isotopes: The isotopes of the two groups (the order should match that of groups).
676
+ :param couplings: A list of individual coupling constants between the atoms of these two groups.
677
+ """
678
+ self.groups = groups
679
+ self.isotopes = isotopes
680
+ self.couplings = couplings
681
+
682
+ @property
683
+ def total(self):
684
+ """
685
+ The total (average coupling) between the atoms of the groups contributing to this coupling.
686
+ """
687
+ return sum([coupling.isotropic('total') for coupling in self.couplings]) / len(self.couplings)
688
+
689
+ def dump(self, digichem_options):
690
+ """
691
+ Get a representation of this result object in primitive format.
692
+ """
693
+ return {
694
+ "groups": [group.label for group in self.groups],
695
+ "isotopes": list(self.isotopes),
696
+ "total": {
697
+ "units": "Hz",
698
+ "value": float(self.total),
699
+ }
700
+ #"couplings": [coupling.dump(digichem_options) for coupling in self.couplings]
701
+ }
702
+
703
+ def other(self, atom_group):
704
+ """
705
+ Get the index of the other atom_group involved in this coupling.
706
+
707
+ :param atom_group: One of the two atom groups.
708
+ """
709
+ return abs(1 - self.groups.index(atom_group))
710
+
711
+ def num_coupled_atoms(self, atom_group):
712
+ """
713
+ Calculate the number of atoms one of the atom groups is coupled to.
714
+ """
715
+ second_index = self.other(atom_group)
716
+ return int(len(self.couplings) / len(atom_group.atoms))
717
+
718
+ def multiplicity(self, atom_group):
719
+ """
720
+ Calculate the multiplicity (number of peaks generated) by this coupling.
721
+
722
+ :param atom_group: The atom_group who's corresponding peak is to be split. This should be one of the two groups in self.groups.
723
+ """
724
+ second_index = self.other(atom_group)
725
+ # Calculate how many peaks are going to be generated.
726
+ # This is the number of equivalent nuclei * (2 * spin) + 1
727
+
728
+ # First, determine the number of atoms beings coupled to.
729
+ # Note that this is not simply the number of atoms in the other atom group, because
730
+ # not all atoms of the main group are necessarily coupled to all atoms of the foreign group
731
+ num_coupled_atoms = self.num_coupled_atoms(atom_group)
732
+
733
+ ele = getattr(periodictable, self.groups[second_index].element.symbol)
734
+ iso = ele[self.isotopes[second_index]]
735
+ iso.neutron
736
+ spin = float(Fraction(iso.nuclear_spin))
737
+ number = num_coupled_atoms * 2 * spin + 1
738
+
739
+ # Multiplicity label
740
+ if number == 1:
741
+ multiplicity = "singlet"
742
+ symbol = "s"
743
+ elif number == 2:
744
+ multiplicity = "doublet"
745
+ symbol = "d"
746
+ elif number == 3:
747
+ multiplicity = "triplet"
748
+ symbol = "t"
749
+ elif number == 4:
750
+ multiplicity = "quartet"
751
+ symbol = "q"
752
+ elif number == 5:
753
+ multiplicity = "pentet"
754
+ symbol = "p"
755
+ elif number == 6:
756
+ multiplicity = "sextet"
757
+ symbol = "sext"
758
+ elif number == 7:
759
+ multiplicity = "septet"
760
+ symbol = "sept"
761
+ elif number == 8:
762
+ multiplicity = "octet"
763
+ symbol = "oct"
764
+ elif number == 9:
765
+ multiplicity = "nonet"
766
+ symbol = "non"
767
+ else:
768
+ multiplicity = "10"
769
+ symbol = "10"
770
+
771
+ return {"symbol": symbol, "number": number, "multiplicity": multiplicity}
772
+
773
+
774
+ class NMR(Result_object, Floatable_mixin):
775
+ """
776
+ A result object containing all the NMR related data for a single atom.
777
+
778
+ For a given atom, this class will contain:
779
+ - The chemical shielding of this atom (including a breakdown by tensors, if available).
780
+ - The spin-spin coupling constants between this atom and all other atoms for which couplings were calculated (also with a breakdown by tensor, if available).
781
+ """
782
+
783
+ def __init__(self, atom, shielding, couplings):
784
+ """
785
+ :param atom: The atom these NMR parameters relate to.
786
+ :param shielding: The chemical shielding object for this atom.
787
+ :param couplings: A dictionary of all the coupling interactions calculated for this atom.
788
+ """
789
+ self.atom = atom
790
+ self.shielding = shielding
791
+ self.couplings = couplings
792
+
793
+ def __float__(self):
794
+ return float(self.shielding.isotropic())
795
+
796
+ # def __eq__(self, other):
797
+ # return self.atom == other.atom and self.shielding == other.shielding and self.couplings == other.couplings
798
+
799
+ @classmethod
800
+ def list_from_parser(self, parser):
801
+ """
802
+ """
803
+ return [self(atom, parser.results.nmr_shieldings[atom], parser.results.nmr_couplings.between(atom)) for atom in parser.results.atoms if atom in parser.results.nmr_shieldings]
804
+
805
+ @classmethod
806
+ def list_from_dump(self, data, result_set, options):
807
+ """
808
+ Get a list of instances of this class from its dumped representation.
809
+
810
+ :param data: The data to parse.
811
+ :param result_set: The partially constructed result set which is being populated.
812
+ """
813
+ return [
814
+ self(
815
+ result_set.atoms.find(nmr_dict['atom']),
816
+ NMR_shielding.from_dump(nmr_dict['shielding'], result_set, options),
817
+ NMR_spin_couplings_list.from_dump(nmr_dict['couplings'], result_set, options)
818
+ ) for nmr_dict in data
819
+ ]
820
+
821
+
822
+ def dump(self, digichem_options):
823
+ """
824
+ Get a representation of this result object in primitive format.
825
+ """
826
+ return {
827
+ "atom": self.atom.label,
828
+ "shielding": self.shielding.dump(digichem_options),
829
+ "couplings": self.couplings.dump(digichem_options)
830
+ }
831
+
832
+
833
+ class NMR_tensor_ABC(Result_object):
834
+ """ABC for classes that contain dicts of NMR tensors."""
835
+
836
+ tensor_names = ()
837
+ units = ""
838
+
839
+ def __init__(self, tensors):
840
+ self.tensors = tensors
841
+
842
+ # This is unused.
843
+ #self.total_isotropic = total_isotropic
844
+
845
+ def eigenvalues(self, tensor = "total", real_only = True):
846
+ """
847
+ Calculate the eigenvalues for a given tensor.
848
+
849
+ :param tensor: The name of a tensor to calculate for (see tensor_names). Use 'total' for the total tensor.
850
+ """
851
+ try:
852
+ return numpy.array([val.real for val in numpy.linalg.eigvals(self.tensors[tensor])])
853
+
854
+ except KeyError:
855
+ if tensor not in self.tensor_names:
856
+ raise ValueError("The tensor '{}' is not recognised") from None
857
+
858
+ elif tensor not in self.tensors:
859
+ raise ValueError("The tensor '{}' is not available") from None
860
+
861
+ def isotropic(self, tensor = "total"):
862
+ """
863
+ Calculate the isotropic value for a given tensor.
864
+
865
+ :param tensor: The name of a tensor to calculate for (see tensor_names). Use 'total' for the total tensor.
866
+ """
867
+ eigenvalues = self.eigenvalues(tensor)
868
+ return sum(eigenvalues) / len(eigenvalues)
869
+
870
+ def dump(self, digichem_options):
871
+ """
872
+ Get a representation of this result object in primitive format.
873
+ """
874
+ return {
875
+ "tensors": {t_type: {"value": list(list(map(float, dim)) for dim in tensor), "units": self.units} for t_type, tensor in self.tensors.items()},
876
+ "eigenvalues": {t_type: {"value": list(list(map(float, self.eigenvalues(t_type)))), "units": self.units} for t_type in self.tensors},
877
+ "isotropic": {t_type: {"value": float(self.isotropic(t_type)), "units": self.units} for t_type in self.tensors}
878
+ }
879
+
880
+
881
+ class NMR_shielding(NMR_tensor_ABC):
882
+ """
883
+ A result object to represent the chemical shielding of an atom.
884
+ """
885
+
886
+ tensor_names = ("paramagnetic", "diamagnetic", "total")
887
+ units = "ppm"
888
+
889
+ def __init__(self, tensors, reference = None):
890
+ """
891
+ :param tensors: A dictionary of tensors.
892
+ :param reference: An optional reference isotropic value to correct this shielding by.
893
+ """
894
+ super().__init__(tensors)
895
+ self.reference = reference
896
+
897
+ def isotropic(self, tensor = "total", correct = True):
898
+ """
899
+ Calculate the isotropic value for a given tensor.
900
+
901
+ :param tensor: The name of a tensor to calculate for (see tensor_names). Use 'total' for the total tensor.
902
+ :param correct: Whether to correct this shielding value by the reference.
903
+ """
904
+ eigenvalues = self.eigenvalues(tensor)
905
+ absolute = sum(eigenvalues) / len(eigenvalues)
906
+ if correct and self.reference is not None:
907
+ return self.reference - absolute
908
+ else:
909
+ return absolute
910
+
911
+ @classmethod
912
+ def dict_from_parser(self, parser):
913
+ """
914
+ Create a dict of NMR shielding objects from an output file parser.
915
+ Each key is the atom being shielded.
916
+
917
+ :param parser: An output file parser.
918
+ :return: A list of NMR_shielding objects. The list will be empty if no NMR data is available.
919
+ """
920
+ shieldings = {}
921
+ try:
922
+ for atom_index, tensors in parser.data.nmrtensors.items():
923
+ total_isotropic = tensors.pop("isotropic")
924
+ shieldings[parser.results.atoms[atom_index]] = self(
925
+ tensors,
926
+ reference = parser.options['nmr']['standards'].get(parser.results.atoms[atom_index].element.symbol, None)
927
+ )
928
+
929
+ except AttributeError:
930
+ return {}
931
+
932
+ return shieldings
933
+
934
+ def dump(self, digichem_options):
935
+ """
936
+ Get a representation of this result object in primitive format.
937
+ """
938
+ dump_dic = {
939
+ "reference": self.reference,
940
+ }
941
+
942
+ dump_dic.update(super().dump(digichem_options))
943
+ return dump_dic
944
+
945
+ @classmethod
946
+ def from_dump(self, data, result_set, options):
947
+ tensors = {
948
+ t_type: tensor_dict['value']
949
+ for t_type, tensor_dict
950
+ in data['tensors'].items()
951
+ }
952
+ return self(
953
+ tensors,
954
+ reference = data['reference']
955
+ )
956
+
957
+
958
+ # We could look at some more advanced type of container for couplings.
959
+ # A dictionary might make sense, but it's difficult to choose how exactly the keys should be arranged and ordered.
960
+ class NMR_spin_couplings_list(Result_container):
961
+ """A collection of NMR spin-spin couplings."""
962
+
963
+ @classmethod
964
+ def from_parser(self, parser):
965
+ """
966
+ Get an NMR_spin_couplings_list object from an output file parser.
967
+
968
+ :param parser: An output file parser.
969
+ :return: A NMR_spin_couplings_list object. The list will be empty if no NMR data is available.
970
+ """
971
+ return self(NMR_spin_coupling.list_from_parser(parser))
972
+
973
+ @classmethod
974
+ def from_dump(self, data, result_set, options):
975
+ return self(NMR_spin_coupling.list_from_dump(data, result_set, options))
976
+
977
+ def find(self, criteria = None, *, label = None, index = None):
978
+ """
979
+ """
980
+ if label is None and index is None and criteria is None:
981
+ raise ValueError("One of 'criteria', 'label' or 'index' must be given")
982
+
983
+ if criteria is not None:
984
+ if str(criteria).isdigit():
985
+ index = int(criteria)
986
+
987
+ else:
988
+ label = criteria
989
+
990
+ # Now get our filter func.
991
+ if label is not None:
992
+ filter_func = lambda coupling: label not in [atom.label for atom in coupling.atoms]
993
+
994
+ elif index is not None:
995
+ filter_func = lambda coupling: index not in [atom.index for atom in coupling.atoms]
996
+
997
+ # Now search.
998
+ found = type(self)(filterfalse(filter_func, self))
999
+
1000
+ if len(found) == 0:
1001
+ if label is not None:
1002
+ criteria_string = "label = '{}'".format(label)
1003
+
1004
+ elif index is not None:
1005
+ criteria_string = "index = '{}'".format(index)
1006
+
1007
+ raise Result_unavailable_error("NMR", "could not NMR data for atom '{}'".format(criteria_string))
1008
+
1009
+ return found
1010
+
1011
+ def between(self, atom1, atom1_isotopes = None, atom2 = None, atom2_isotopes = None):
1012
+ """
1013
+ Return a list containing all couplings involving either one or two atoms.
1014
+ """
1015
+ return type(self)([coupling for coupling in self
1016
+ if atom1 in coupling.atoms and
1017
+ (atom2 is None or atom2 in coupling.atoms) and
1018
+ (atom1_isotopes is None or coupling.isotopes[coupling.atoms.index(atom1)] in atom1_isotopes) and
1019
+ (atom2_isotopes is None or atom2 is None or coupling.isotopes[coupling.atoms.index(atom2)] in atom2_isotopes)
1020
+ ])
1021
+
1022
+ class NMR_spin_coupling(NMR_tensor_ABC):
1023
+ """
1024
+ A result object to represent spin-spin NMR couplings.
1025
+ """
1026
+
1027
+ tensor_names = ("paramagnetic", "diamagnetic", "fermi", "spin-dipolar", "spin-dipolar-fermi", "total")
1028
+ units = "Hz"
1029
+
1030
+ def __init__(self, atoms, isotopes, tensors):
1031
+ """
1032
+ :param atoms: Tuple of atoms that this coupling is between.
1033
+ :param isotopes: Tuple of the specific isotopes of atoms.
1034
+ :param tensors: A dictionary of tensors.
1035
+ """
1036
+ super().__init__(tensors)
1037
+ self.atoms = atoms
1038
+ self.isotopes = isotopes
1039
+
1040
+ @classmethod
1041
+ def list_from_parser(self, parser):
1042
+ """
1043
+ Create a list of NMR coupling objects from an output file parser.
1044
+
1045
+ :param parser: An output file parser.
1046
+ :return: A list of NMR_spin_coupling objects. The list will be empty if no NMR data is available.
1047
+ """
1048
+ couplings = []
1049
+ try:
1050
+ for atom_tuple, isotopes in parser.data.nmrcouplingtensors.items():
1051
+ for isotope_tuple, tensors in isotopes.items():
1052
+ total_isotropic = tensors.pop("isotropic")
1053
+ couplings.append(self((parser.results.atoms[atom_tuple[0]], parser.results.atoms[atom_tuple[1]]), isotope_tuple, tensors))
1054
+
1055
+ except AttributeError:
1056
+ return []
1057
+
1058
+ return couplings
1059
+
1060
+ @classmethod
1061
+ def list_from_dump(self, data, result_set, options):
1062
+ return [
1063
+ self(
1064
+ tuple(result_set.atoms.find(atom_label) for atom_label in dump_dict['atoms']),
1065
+ tuple(dump_dict['isotopes']),
1066
+ {
1067
+ t_type: tensor_dict['value']
1068
+ for t_type, tensor_dict
1069
+ in dump_dict['tensors'].items()
1070
+ }
1071
+ )
1072
+ for dump_dict
1073
+ in data
1074
+ ]
1075
+
1076
+ def dump(self, digichem_options):
1077
+ """
1078
+ Get a representation of this result object in primitive format.
1079
+ """
1080
+ dump_dic = {
1081
+ "atoms": (self.atoms[0].label, self.atoms[1].label),
1082
+ "isotopes": (self.isotopes[0], self.isotopes[1]),
1083
+ }
1084
+
1085
+ dump_dic.update(super().dump(digichem_options))
1086
+ return dump_dic