digichem-core 7.0.4__py3-none-any.whl → 7.3.0__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 +2 -11
- digichem/config/base.py +2 -2
- digichem/datas.py +6 -2
- digichem/image/render.py +2 -1
- digichem/image/spectroscopy.py +15 -8
- digichem/input/__init__.py +1 -1
- digichem/input/digichem_input.py +33 -1
- digichem/input/gaussian.py +41 -1
- digichem/misc/base.py +12 -0
- digichem/misc/io.py +8 -3
- digichem/parse/base.py +135 -4
- digichem/parse/cclib.py +2 -115
- digichem/parse/censo.py +135 -0
- digichem/parse/crest.py +89 -0
- digichem/parse/gaussian.py +2 -2
- digichem/parse/pyscf.py +15 -4
- digichem/result/atom.py +37 -20
- digichem/result/metadata.py +3 -0
- digichem/result/nmr.py +103 -27
- digichem/result/spectroscopy.py +2 -2
- digichem/test/test_input.py +2 -1
- digichem/test/test_result.py +2 -2
- digichem/test/util.py +1 -0
- {digichem_core-7.0.4.dist-info → digichem_core-7.3.0.dist-info}/METADATA +3 -3
- {digichem_core-7.0.4.dist-info → digichem_core-7.3.0.dist-info}/RECORD +28 -26
- {digichem_core-7.0.4.dist-info → digichem_core-7.3.0.dist-info}/WHEEL +1 -1
- {digichem_core-7.0.4.dist-info → digichem_core-7.3.0.dist-info}/licenses/LICENSE +1 -1
- {digichem_core-7.0.4.dist-info → digichem_core-7.3.0.dist-info}/licenses/COPYING.md +0 -0
digichem/parse/censo.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import datetime
|
|
4
|
+
import periodictable
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
import types
|
|
7
|
+
|
|
8
|
+
from digichem.parse.base import Parser_abc
|
|
9
|
+
import digichem.log
|
|
10
|
+
from digichem.input.digichem_input import si_from_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Censo_parser(Parser_abc):
|
|
14
|
+
"""
|
|
15
|
+
Top level class for parsing output from censo data.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, mol_name, program, **kwargs):
|
|
19
|
+
self.program = program
|
|
20
|
+
self.mol_name = mol_name
|
|
21
|
+
super().__init__(**kwargs)
|
|
22
|
+
|
|
23
|
+
def _parse(self):
|
|
24
|
+
"""
|
|
25
|
+
Extract results from our output files.
|
|
26
|
+
"""
|
|
27
|
+
self.data = types.SimpleNamespace()
|
|
28
|
+
|
|
29
|
+
# Censo calculations are normally made up of four distinct phases (prescreening, screening, optimisation, and refinement).
|
|
30
|
+
calculations = []
|
|
31
|
+
methods = []
|
|
32
|
+
functional = []
|
|
33
|
+
basis_set = []
|
|
34
|
+
engines = []
|
|
35
|
+
|
|
36
|
+
if self.program.calculation.properties['prescreening']['calc']:
|
|
37
|
+
calculations.append("Screening")
|
|
38
|
+
methods.append(self.program.calculation.properties['prescreening']['gfn'])
|
|
39
|
+
methods.append("DFT")
|
|
40
|
+
engines.append(self.program.calculation.properties['prescreening']['engine'])
|
|
41
|
+
functional.append(self.program.calculation.properties['prescreening']['functional'])
|
|
42
|
+
basis_set.append(self.program.calculation.properties['prescreening']['basis_set'])
|
|
43
|
+
|
|
44
|
+
if self.program.calculation.properties['screening']['calc']:
|
|
45
|
+
calculations.append("Screening")
|
|
46
|
+
methods.append(self.program.calculation.properties['screening']['gfn'])
|
|
47
|
+
methods.append("DFT")
|
|
48
|
+
engines.append(self.program.calculation.properties['screening']['engine'])
|
|
49
|
+
functional.append(self.program.calculation.properties['screening']['functional'])
|
|
50
|
+
basis_set.append(self.program.calculation.properties['screening']['basis_set'])
|
|
51
|
+
|
|
52
|
+
if self.program.calculation.properties['optimisation']['calc']:
|
|
53
|
+
calculations.append("Optimisation")
|
|
54
|
+
methods.append(self.program.calculation.properties['optimisation']['gfn'])
|
|
55
|
+
methods.append("DFT")
|
|
56
|
+
engines.append(self.program.calculation.properties['optimisation']['engine'])
|
|
57
|
+
functional.append(self.program.calculation.properties['optimisation']['functional'])
|
|
58
|
+
basis_set.append(self.program.calculation.properties['optimisation']['basis_set'])
|
|
59
|
+
|
|
60
|
+
if self.program.calculation.properties['refinement']['calc']:
|
|
61
|
+
calculations.append("Single Point")
|
|
62
|
+
methods.append(self.program.calculation.properties['refinement']['gfn'])
|
|
63
|
+
methods.append("DFT")
|
|
64
|
+
engines.append(self.program.calculation.properties['refinement']['engine'])
|
|
65
|
+
functional.append(self.program.calculation.properties['refinement']['functional'])
|
|
66
|
+
basis_set.append(self.program.calculation.properties['refinement']['basis_set'])
|
|
67
|
+
|
|
68
|
+
engines = ["CENSO"] + list(set(engines))
|
|
69
|
+
|
|
70
|
+
# Metadata we can get entirely from our passed in program object.
|
|
71
|
+
self.data.metadata = {
|
|
72
|
+
"name": self.mol_name,
|
|
73
|
+
"jobId": self.program.calculation.job_id,
|
|
74
|
+
"wall_time": [self.program.duration],
|
|
75
|
+
"cpu_time": [self.program.duration * self.program.calculation.performance['num_cpu']],
|
|
76
|
+
"date": datetime.datetime.now(datetime.timezone.utc).timestamp(),
|
|
77
|
+
"package": "/".join(engines),
|
|
78
|
+
#"package_version": None,
|
|
79
|
+
"calculations": calculations,
|
|
80
|
+
"success": not self.program.error,
|
|
81
|
+
"methods": methods,
|
|
82
|
+
"functional": "/".join(functional),
|
|
83
|
+
"basis_set": "/".join(basis_set),
|
|
84
|
+
"charge": self.program.calculation.charge,
|
|
85
|
+
"multiplicity": self.program.calculation.multiplicity,
|
|
86
|
+
"optimisation_converged": not self.program.error,
|
|
87
|
+
#"temperature": None,
|
|
88
|
+
#"pressure": None,
|
|
89
|
+
"orbital_spin_type": "restricted" if self.program.calculation.multiplicity == 1 else "unrestricted",
|
|
90
|
+
"solvent_name": str(self.program.calculation.solution['solvent']) if self.program.calculation.solution['calc'] else None,
|
|
91
|
+
"solvent_model": self.program.calculation.solution['model'] if self.program.calculation.solution['calc'] else None,
|
|
92
|
+
"num_cpu": self.program.calculation.performance['num_cpu'],
|
|
93
|
+
#"memory_used": None,
|
|
94
|
+
"memory_available": self.program.calculation.performance['memory'],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
coord_file = self.program.next_coords
|
|
99
|
+
|
|
100
|
+
except Exception:
|
|
101
|
+
coord_file = None
|
|
102
|
+
|
|
103
|
+
if coord_file:
|
|
104
|
+
main_si = si_from_file(coord_file, gen3D = False, charge = self.program.calculation.charge, multiplicity = self.program.calculation.multiplicity)
|
|
105
|
+
|
|
106
|
+
self.data.atomnos = []
|
|
107
|
+
self.data.atomcoords = [[]]
|
|
108
|
+
|
|
109
|
+
for atom in main_si.atoms:
|
|
110
|
+
self.data.atomnos.append(periodictable.elements.symbol(atom['atom']).number)
|
|
111
|
+
self.data.atomcoords[0].append(
|
|
112
|
+
[
|
|
113
|
+
atom['x'],
|
|
114
|
+
atom['y'],
|
|
115
|
+
atom['z']
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def post_parse(self):
|
|
120
|
+
"""
|
|
121
|
+
Perform any required operations after line-by-line parsing.
|
|
122
|
+
"""
|
|
123
|
+
super().post_parse()
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Try to generate a checksum from metadata.
|
|
127
|
+
self.data._id = hashlib.sha1(json.dumps(self.data.metadata, sort_keys = True, default = str).encode('utf-8')).hexdigest()
|
|
128
|
+
|
|
129
|
+
except Exception:
|
|
130
|
+
# No luck, something in metadata must be unhashable.
|
|
131
|
+
digichem.log.get_logger().error("Unable to generate hash ID from calculation metadata, using random ID instead", exc_info = True)
|
|
132
|
+
# TODO: Think of a better way to do this.
|
|
133
|
+
self.data._id = hashlib.sha1(uuid4().hex.encode('utf-8')).hexdigest()
|
|
134
|
+
|
|
135
|
+
|
digichem/parse/crest.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import datetime
|
|
4
|
+
import periodictable
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
import types
|
|
7
|
+
|
|
8
|
+
from digichem.parse.base import Parser_abc
|
|
9
|
+
import digichem.log
|
|
10
|
+
from digichem.input.digichem_input import si_from_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Crest_parser(Parser_abc):
|
|
15
|
+
"""
|
|
16
|
+
Top level class for parsing output from crest data.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, mol_name, program, **kwargs):
|
|
20
|
+
self.program = program
|
|
21
|
+
self.mol_name = mol_name
|
|
22
|
+
super().__init__(**kwargs)
|
|
23
|
+
|
|
24
|
+
def _parse(self):
|
|
25
|
+
"""
|
|
26
|
+
Extract results from our output files.
|
|
27
|
+
"""
|
|
28
|
+
self.data = types.SimpleNamespace()
|
|
29
|
+
|
|
30
|
+
# Metadata we can get entirely from our passed in program object.
|
|
31
|
+
self.data.metadata = {
|
|
32
|
+
"name": self.mol_name,
|
|
33
|
+
"jobId": self.program.calculation.job_id,
|
|
34
|
+
"wall_time": [self.program.duration],
|
|
35
|
+
"cpu_time": [self.program.duration * self.program.calculation.performance['num_cpu']],
|
|
36
|
+
"date": datetime.datetime.now(datetime.timezone.utc).timestamp(),
|
|
37
|
+
"package": "CREST",
|
|
38
|
+
#"package_version": None,
|
|
39
|
+
"calculations": ["Optimisation"],
|
|
40
|
+
"success": not self.program.error,
|
|
41
|
+
"methods": [self.program.calculation.method['gfn']['level']],
|
|
42
|
+
"charge": self.program.calculation.charge,
|
|
43
|
+
"multiplicity": self.program.calculation.multiplicity,
|
|
44
|
+
"optimisation_converged": not self.program.error,
|
|
45
|
+
#"temperature": None,
|
|
46
|
+
#"pressure": None,
|
|
47
|
+
# TODO: CREST doesn't actually use orbitals...
|
|
48
|
+
"orbital_spin_type": "restricted",
|
|
49
|
+
"solvent_name": str(self.program.calculation.solution['solvent']) if self.program.calculation.solution['calc'] else None,
|
|
50
|
+
"solvent_model": self.program.calculation.solution['model'] if self.program.calculation.solution['calc'] else None,
|
|
51
|
+
"num_cpu": self.program.calculation.performance['num_cpu'],
|
|
52
|
+
#"memory_used": None,
|
|
53
|
+
"memory_available": self.program.calculation.performance['memory'],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Crest calculations are normally used for conformer searching, so we should expect multiple structures as output.
|
|
57
|
+
# We'll use the lowest energy conformer as our 'main' structure.
|
|
58
|
+
main_si = si_from_file(self.program.working_directory / "crest_conformers.xyz", gen3D = False, charge = self.program.calculation.charge, multiplicity = self.program.calculation.multiplicity)
|
|
59
|
+
|
|
60
|
+
self.data.atomnos = []
|
|
61
|
+
self.data.atomcoords = [[]]
|
|
62
|
+
|
|
63
|
+
for atom in main_si.atoms:
|
|
64
|
+
self.data.atomnos.append(periodictable.elements.symbol(atom['atom']).number)
|
|
65
|
+
self.data.atomcoords[0].append(
|
|
66
|
+
[
|
|
67
|
+
atom['x'],
|
|
68
|
+
atom['y'],
|
|
69
|
+
atom['z']
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def post_parse(self):
|
|
74
|
+
"""
|
|
75
|
+
Perform any required operations after line-by-line parsing.
|
|
76
|
+
"""
|
|
77
|
+
super().post_parse()
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Try to generate a checksum from metadata.
|
|
81
|
+
self.data._id = hashlib.sha1(json.dumps(self.data.metadata, sort_keys = True, default = str).encode('utf-8')).hexdigest()
|
|
82
|
+
|
|
83
|
+
except Exception:
|
|
84
|
+
# No luck, something in metadata must be unhashable.
|
|
85
|
+
digichem.log.get_logger().error("Unable to generate hash ID from calculation metadata, using random ID instead", exc_info = True)
|
|
86
|
+
# TODO: Think of a better way to do this.
|
|
87
|
+
self.data._id = hashlib.sha1(uuid4().hex.encode('utf-8')).hexdigest()
|
|
88
|
+
|
|
89
|
+
|
digichem/parse/gaussian.py
CHANGED
|
@@ -29,9 +29,9 @@ class Gaussian_parser(Cclib_parser):
|
|
|
29
29
|
CPU_TIME_HEADER = "Job cpu time:"
|
|
30
30
|
CPU_HEADER = "Will use up to"
|
|
31
31
|
|
|
32
|
-
def __init__(self, *log_files, rwfdump = "rwfdump", options, **auxiliary_files):
|
|
32
|
+
def __init__(self, *log_files, rwfdump = "rwfdump", options, metadata_defaults = None, **auxiliary_files):
|
|
33
33
|
self.rwfdump = rwfdump
|
|
34
|
-
super().__init__(*log_files, options = options, **auxiliary_files)
|
|
34
|
+
super().__init__(*log_files, options = options, metadata_defaults = metadata_defaults, **auxiliary_files)
|
|
35
35
|
|
|
36
36
|
def parse_metadata(self):
|
|
37
37
|
"""
|
digichem/parse/pyscf.py
CHANGED
|
@@ -7,6 +7,7 @@ from cclib.bridge.cclib2pyscf import cclibfrommethods
|
|
|
7
7
|
from digichem.parse.base import Parser_abc
|
|
8
8
|
import digichem.log
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
class Pyscf_parser(Parser_abc):
|
|
11
12
|
"""
|
|
12
13
|
Top level class for parsing output from pyscf data.
|
|
@@ -18,11 +19,23 @@ class Pyscf_parser(Parser_abc):
|
|
|
18
19
|
super().__init__(**kwargs)
|
|
19
20
|
|
|
20
21
|
def _parse(self):
|
|
22
|
+
"""
|
|
23
|
+
Extract results from our output files.
|
|
24
|
+
"""
|
|
21
25
|
self.data = cclibfrommethods(**self.methods)
|
|
22
|
-
|
|
26
|
+
|
|
27
|
+
def post_parse(self):
|
|
28
|
+
"""
|
|
29
|
+
Perform any required operations after line-by-line parsing.
|
|
30
|
+
"""
|
|
31
|
+
super().post_parse()
|
|
32
|
+
# Set some metadata objects.
|
|
33
|
+
self.data.metadata['name'] = self.mol_name
|
|
34
|
+
self.data._aux = {'methods': self.methods}
|
|
35
|
+
|
|
23
36
|
try:
|
|
24
37
|
# Try to generate a checksum from metadata.
|
|
25
|
-
self.data._id = hashlib.sha1(json.dumps(self.data.metadata, sort_keys = True).encode('utf-8')).hexdigest()
|
|
38
|
+
self.data._id = hashlib.sha1(json.dumps(self.data.metadata, sort_keys = True, default = str).encode('utf-8')).hexdigest()
|
|
26
39
|
|
|
27
40
|
except Exception:
|
|
28
41
|
# No luck, something in metadata must be unhashable.
|
|
@@ -30,6 +43,4 @@ class Pyscf_parser(Parser_abc):
|
|
|
30
43
|
# TODO: Think of a better way to do this.
|
|
31
44
|
self.data._id = hashlib.sha1(uuid4().hex.encode('utf-8')).hexdigest()
|
|
32
45
|
|
|
33
|
-
self.data.metadata['name'] = self.mol_name
|
|
34
|
-
self.data._aux = {'methods': self.methods}
|
|
35
46
|
|
digichem/result/atom.py
CHANGED
|
@@ -255,6 +255,14 @@ class Atom_list(Result_container, Unmergeable_container_mixin, Molecule_mixin):
|
|
|
255
255
|
|
|
256
256
|
return self._groups
|
|
257
257
|
|
|
258
|
+
@property
|
|
259
|
+
def bond_matrix(self):
|
|
260
|
+
"""
|
|
261
|
+
Get the bond distance matrix for this molecule, which indicates how many bonds separate each atom in the molecule.
|
|
262
|
+
"""
|
|
263
|
+
from rdkit import Chem
|
|
264
|
+
return Chem.rdmolops.GetDistanceMatrix(self.to_rdkit_molecule())
|
|
265
|
+
|
|
258
266
|
def find(self, criteria = None, *, label = None, index = None):
|
|
259
267
|
"""
|
|
260
268
|
Find an atom that matches a given criteria
|
|
@@ -310,28 +318,30 @@ class Atom_list(Result_container, Unmergeable_container_mixin, Molecule_mixin):
|
|
|
310
318
|
return self._smiles
|
|
311
319
|
|
|
312
320
|
except AttributeError:
|
|
313
|
-
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
# Cache miss, go do some work.
|
|
314
324
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
325
|
+
from rdkit.Chem import MolToSmiles
|
|
326
|
+
from rdkit.Chem.rdmolops import RemoveHs
|
|
327
|
+
|
|
328
|
+
# TODO: Find some other way of generating SMILES.
|
|
319
329
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
mol = self.to_rdkit_molecule()
|
|
331
|
+
try:
|
|
332
|
+
# TODO: rdkit is unreliable, this method can fail for lots of reasons...
|
|
333
|
+
mol = RemoveHs(mol)
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
self._smiles = MolToSmiles(mol)
|
|
338
|
+
return self._smiles
|
|
329
339
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
340
|
+
# # TODO: Handle cases where obabel isn't available
|
|
341
|
+
# conv = Openprattle_converter.get_cls("xyz")(input_file = self.to_xyz(), input_file_type = "xyz")
|
|
342
|
+
# # Cache the result in case we need it again.
|
|
343
|
+
# self._smiles = conv.convert("can").strip()
|
|
344
|
+
# return self._smiles
|
|
335
345
|
|
|
336
346
|
@property
|
|
337
347
|
def X_length(self):
|
|
@@ -559,9 +569,16 @@ class Atom_list(Result_container, Unmergeable_container_mixin, Molecule_mixin):
|
|
|
559
569
|
rdDetermineBonds.DetermineBonds(mol, charge = self.charge)
|
|
560
570
|
|
|
561
571
|
except Exception:
|
|
572
|
+
#formula_string may also not be implemented...
|
|
573
|
+
try:
|
|
574
|
+
formula_string = self.formula_string
|
|
575
|
+
|
|
576
|
+
except Exception:
|
|
577
|
+
formula_string = None
|
|
578
|
+
|
|
562
579
|
# This function is not implemented for some atoms (eg, Se).
|
|
563
580
|
digichem.log.get_logger().warning(
|
|
564
|
-
"Unable to determine bond ordering for
|
|
581
|
+
"Unable to determine bond ordering for '{}'; all bonds will be represented as single bonds only".format(formula_string)
|
|
565
582
|
, exc_info = True
|
|
566
583
|
)
|
|
567
584
|
|
digichem/result/metadata.py
CHANGED
|
@@ -354,6 +354,9 @@ class Metadata(Result_object):
|
|
|
354
354
|
calculations = []
|
|
355
355
|
if "Single Point" in self.calculations:
|
|
356
356
|
calculations.append("single point energy")
|
|
357
|
+
|
|
358
|
+
if "Screening" in self.calculations:
|
|
359
|
+
calculations.append("conformer screening")
|
|
357
360
|
|
|
358
361
|
if "Optimisation" in self.calculations:
|
|
359
362
|
calculations.append("optimised structure")
|
digichem/result/nmr.py
CHANGED
|
@@ -5,6 +5,7 @@ from fractions import Fraction
|
|
|
5
5
|
import re
|
|
6
6
|
import statistics
|
|
7
7
|
import math
|
|
8
|
+
from configurables.misc import is_int
|
|
8
9
|
|
|
9
10
|
from digichem.misc.base import regular_range, powerset
|
|
10
11
|
from digichem.exception.base import Result_unavailable_error
|
|
@@ -545,6 +546,9 @@ class NMR_list(Result_container):
|
|
|
545
546
|
# First, decide which atoms are actually equivalent.
|
|
546
547
|
# We can do this by comparing canonical SMILES groupings.
|
|
547
548
|
atom_groups = self.atoms.groups
|
|
549
|
+
|
|
550
|
+
# Also get the bond distance matrix for the molecule, so we can work out which couplings are actually equivalent.
|
|
551
|
+
bond_matrix = self.atoms.bond_matrix
|
|
548
552
|
|
|
549
553
|
nmr_groups = {}
|
|
550
554
|
# Next, assemble group objects.
|
|
@@ -565,12 +569,34 @@ class NMR_list(Result_container):
|
|
|
565
569
|
# We need to do this after initial group assembly in order to discard self coupling.
|
|
566
570
|
# Get unique couplings (so we don't consider any twice).
|
|
567
571
|
group_couplings = {}
|
|
568
|
-
unique_couplings = {(coupling.atoms, coupling.isotopes): coupling for group in nmr_groups.values() for coupling in group['couplings']}.values()
|
|
572
|
+
unique_couplings = list({(coupling.atoms, coupling.isotopes): coupling for group in nmr_groups.values() for coupling in group['couplings']}.values())
|
|
569
573
|
for coupling in unique_couplings:
|
|
570
574
|
# Find the group numbers that correspond to the two atoms in the coupling.
|
|
571
|
-
coupling_groups = tuple(
|
|
572
|
-
|
|
573
|
-
|
|
575
|
+
coupling_groups = tuple(
|
|
576
|
+
[atom_group.id for atom_group in atom_groups.values() if atom in atom_group.atoms][0] for atom in coupling.atoms
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# We need to ensure that coupling_groups is a unique representation of the coupling,
|
|
580
|
+
# the ordering should be fixed.
|
|
581
|
+
indices = [
|
|
582
|
+
int(coupling_groups[0] >= coupling_groups[1]),
|
|
583
|
+
int(coupling_groups[0] < coupling_groups[1])
|
|
584
|
+
]
|
|
585
|
+
coupling_groups = (
|
|
586
|
+
coupling_groups[indices[0]],
|
|
587
|
+
coupling_groups[indices[1]]
|
|
588
|
+
)
|
|
589
|
+
isotopes = (
|
|
590
|
+
coupling.isotopes[indices[0]],
|
|
591
|
+
coupling.isotopes[indices[1]]
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# The group key contains the two atom groups, and the distance between them.
|
|
595
|
+
coupling_groups = (
|
|
596
|
+
coupling_groups[0],
|
|
597
|
+
coupling_groups[1],
|
|
598
|
+
float(bond_matrix[coupling.atoms[0].index -1][coupling.atoms[1].index -1])
|
|
599
|
+
)
|
|
574
600
|
|
|
575
601
|
# Append the isotropic coupling constant to the group.
|
|
576
602
|
if coupling_groups not in group_couplings:
|
|
@@ -582,12 +608,14 @@ class NMR_list(Result_container):
|
|
|
582
608
|
group_couplings[coupling_groups][isotopes].append(coupling)
|
|
583
609
|
|
|
584
610
|
# Average each 'equivalent' coupling.
|
|
585
|
-
|
|
611
|
+
average_couplings = {
|
|
586
612
|
group_key: {
|
|
587
613
|
isotope_key: NMR_group_spin_coupling(
|
|
588
|
-
|
|
614
|
+
# TODO: Add in bond distance.
|
|
615
|
+
groups = [atom_groups[group_sub_key] for group_sub_key in group_key[:2]],
|
|
589
616
|
isotopes = isotope_key,
|
|
590
|
-
couplings = isotope_couplings
|
|
617
|
+
couplings = isotope_couplings,
|
|
618
|
+
distance = group_key[2]
|
|
591
619
|
) for isotope_key, isotope_couplings in isotopes.items()}
|
|
592
620
|
for group_key, isotopes in group_couplings.items()
|
|
593
621
|
}
|
|
@@ -599,11 +627,11 @@ class NMR_list(Result_container):
|
|
|
599
627
|
|
|
600
628
|
coupling = [
|
|
601
629
|
isotope_coupling
|
|
602
|
-
for group_key, group_coupling in
|
|
630
|
+
for group_key, group_coupling in average_couplings.items()
|
|
603
631
|
for isotope_coupling in group_coupling.values()
|
|
604
|
-
if group_id in group_key
|
|
632
|
+
if group_id in group_key[:2]
|
|
605
633
|
]
|
|
606
|
-
nmr_object_groups[raw_group['group']] =
|
|
634
|
+
nmr_object_groups[raw_group['group']] = NMR_group(raw_group['group'], raw_group['shieldings'], coupling)
|
|
607
635
|
|
|
608
636
|
return nmr_object_groups
|
|
609
637
|
|
|
@@ -650,7 +678,10 @@ class NMR_group(Result_object, Floatable_mixin):
|
|
|
650
678
|
def __init__(self, group, shieldings, couplings):
|
|
651
679
|
self.group = group
|
|
652
680
|
self.shieldings = shieldings
|
|
653
|
-
self.couplings =
|
|
681
|
+
self.couplings = sorted(
|
|
682
|
+
couplings,
|
|
683
|
+
key = lambda coupling: abs(coupling.total)
|
|
684
|
+
)
|
|
654
685
|
|
|
655
686
|
# Calculate average shieldings and couplings.
|
|
656
687
|
self.shielding = float(sum([shielding.isotropic("total") for shielding in shieldings]) / len(shieldings))
|
|
@@ -678,15 +709,17 @@ class NMR_group_spin_coupling(Result_object):
|
|
|
678
709
|
A result object containing the average coupling between two different groups of nuclei.
|
|
679
710
|
"""
|
|
680
711
|
|
|
681
|
-
def __init__(self, groups, isotopes, couplings):
|
|
712
|
+
def __init__(self, groups, isotopes, couplings, distance = None):
|
|
682
713
|
"""
|
|
683
714
|
:param groups: The two atom groups that this coupling is between.
|
|
684
715
|
:param isotopes: The isotopes of the two groups (the order should match that of groups).
|
|
685
716
|
:param couplings: A list of individual coupling constants between the atoms of these two groups.
|
|
717
|
+
:param distance: The bond distance between the two atoms.
|
|
686
718
|
"""
|
|
687
719
|
self.groups = groups
|
|
688
720
|
self.isotopes = isotopes
|
|
689
721
|
self.couplings = couplings
|
|
722
|
+
self.distance = int(distance) if distance is not None and is_int(distance) else distance
|
|
690
723
|
|
|
691
724
|
@property
|
|
692
725
|
def total(self):
|
|
@@ -705,6 +738,10 @@ class NMR_group_spin_coupling(Result_object):
|
|
|
705
738
|
"total": {
|
|
706
739
|
"units": "Hz",
|
|
707
740
|
"value": float(self.total),
|
|
741
|
+
},
|
|
742
|
+
"distance": {
|
|
743
|
+
"units": "bonds",
|
|
744
|
+
"value": self.distance
|
|
708
745
|
}
|
|
709
746
|
#"couplings": [coupling.dump(digichem_options, all) for coupling in self.couplings]
|
|
710
747
|
}
|
|
@@ -721,7 +758,19 @@ class NMR_group_spin_coupling(Result_object):
|
|
|
721
758
|
"""
|
|
722
759
|
Calculate the number of atoms one of the atom groups is coupled to.
|
|
723
760
|
"""
|
|
724
|
-
second_index = self.other(atom_group)
|
|
761
|
+
# second_index = self.other(atom_group)
|
|
762
|
+
|
|
763
|
+
# if [group.label for group in self.groups] == ["H9", "H10"] or [group.label for group in self.groups] == ["H10", "H9"]:
|
|
764
|
+
# print()
|
|
765
|
+
|
|
766
|
+
# atoms = []
|
|
767
|
+
# for coupling in self.couplings:
|
|
768
|
+
# for atom in coupling.atoms:
|
|
769
|
+
# if atom not in atom_group.atoms:
|
|
770
|
+
# atoms.append(atom)
|
|
771
|
+
|
|
772
|
+
# return len(list(set(atoms)))
|
|
773
|
+
|
|
725
774
|
return int(len(self.couplings) / len(atom_group.atoms))
|
|
726
775
|
|
|
727
776
|
def multiplicity(self, atom_group):
|
|
@@ -845,11 +894,11 @@ class NMR_tensor_ABC(Result_object):
|
|
|
845
894
|
tensor_names = ()
|
|
846
895
|
units = ""
|
|
847
896
|
|
|
848
|
-
def __init__(self, tensors):
|
|
897
|
+
def __init__(self, tensors, total_isotropic = None):
|
|
849
898
|
self.tensors = tensors
|
|
850
899
|
|
|
851
900
|
# This is unused.
|
|
852
|
-
|
|
901
|
+
self.total_isotropic = total_isotropic
|
|
853
902
|
|
|
854
903
|
def eigenvalues(self, tensor = "total", real_only = True):
|
|
855
904
|
"""
|
|
@@ -862,10 +911,10 @@ class NMR_tensor_ABC(Result_object):
|
|
|
862
911
|
|
|
863
912
|
except KeyError:
|
|
864
913
|
if tensor not in self.tensor_names:
|
|
865
|
-
raise ValueError("The tensor '{}' is not recognised") from None
|
|
914
|
+
raise ValueError("The tensor '{}' is not recognised".format(tensor)) from None
|
|
866
915
|
|
|
867
916
|
elif tensor not in self.tensors:
|
|
868
|
-
raise ValueError("The tensor '{}' is not available") from None
|
|
917
|
+
raise ValueError("The tensor '{}' is not available".format(tensor)) from None
|
|
869
918
|
|
|
870
919
|
def isotropic(self, tensor = "total"):
|
|
871
920
|
"""
|
|
@@ -873,8 +922,17 @@ class NMR_tensor_ABC(Result_object):
|
|
|
873
922
|
|
|
874
923
|
:param tensor: The name of a tensor to calculate for (see tensor_names). Use 'total' for the total tensor.
|
|
875
924
|
"""
|
|
876
|
-
|
|
877
|
-
|
|
925
|
+
try:
|
|
926
|
+
eigenvalues = self.eigenvalues(tensor)
|
|
927
|
+
return sum(eigenvalues) / len(eigenvalues)
|
|
928
|
+
|
|
929
|
+
except ValueError:
|
|
930
|
+
if tensor == "total" and self.total_isotropic is not None:
|
|
931
|
+
# Use the fallback.
|
|
932
|
+
return self.total_isotropic
|
|
933
|
+
|
|
934
|
+
else:
|
|
935
|
+
raise
|
|
878
936
|
|
|
879
937
|
def _dump_(self, digichem_options, all):
|
|
880
938
|
"""
|
|
@@ -895,12 +953,12 @@ class NMR_shielding(NMR_tensor_ABC):
|
|
|
895
953
|
tensor_names = ("paramagnetic", "diamagnetic", "total")
|
|
896
954
|
units = "ppm"
|
|
897
955
|
|
|
898
|
-
def __init__(self, tensors, reference = None):
|
|
956
|
+
def __init__(self, tensors, reference = None, total_isotropic = None):
|
|
899
957
|
"""
|
|
900
958
|
:param tensors: A dictionary of tensors.
|
|
901
959
|
:param reference: An optional reference isotropic value to correct this shielding by.
|
|
902
960
|
"""
|
|
903
|
-
super().__init__(tensors)
|
|
961
|
+
super().__init__(tensors, total_isotropic)
|
|
904
962
|
self.reference = reference
|
|
905
963
|
|
|
906
964
|
def isotropic(self, tensor = "total", correct = True):
|
|
@@ -910,8 +968,18 @@ class NMR_shielding(NMR_tensor_ABC):
|
|
|
910
968
|
:param tensor: The name of a tensor to calculate for (see tensor_names). Use 'total' for the total tensor.
|
|
911
969
|
:param correct: Whether to correct this shielding value by the reference.
|
|
912
970
|
"""
|
|
913
|
-
|
|
914
|
-
|
|
971
|
+
try:
|
|
972
|
+
eigenvalues = self.eigenvalues(tensor)
|
|
973
|
+
absolute = sum(eigenvalues) / len(eigenvalues)
|
|
974
|
+
|
|
975
|
+
except ValueError:
|
|
976
|
+
if tensor == "total" and self.total_isotropic is not None:
|
|
977
|
+
# Use the fallback.
|
|
978
|
+
absolute = self.total_isotropic
|
|
979
|
+
|
|
980
|
+
else:
|
|
981
|
+
raise None
|
|
982
|
+
|
|
915
983
|
if correct and self.reference is not None:
|
|
916
984
|
return self.reference - absolute
|
|
917
985
|
else:
|
|
@@ -932,7 +1000,8 @@ class NMR_shielding(NMR_tensor_ABC):
|
|
|
932
1000
|
total_isotropic = tensors.pop("isotropic")
|
|
933
1001
|
shieldings[parser.results.atoms[atom_index]] = self(
|
|
934
1002
|
tensors,
|
|
935
|
-
reference = parser.options['nmr']['standards'].get(parser.results.atoms[atom_index].element.symbol, None)
|
|
1003
|
+
reference = parser.options['nmr']['standards'].get(parser.results.atoms[atom_index].element.symbol, None),
|
|
1004
|
+
total_isotropic = total_isotropic
|
|
936
1005
|
)
|
|
937
1006
|
|
|
938
1007
|
except AttributeError:
|
|
@@ -1036,13 +1105,13 @@ class NMR_spin_coupling(NMR_tensor_ABC):
|
|
|
1036
1105
|
tensor_names = ("paramagnetic", "diamagnetic", "fermi", "spin-dipolar", "spin-dipolar-fermi", "total")
|
|
1037
1106
|
units = "Hz"
|
|
1038
1107
|
|
|
1039
|
-
def __init__(self, atoms, isotopes, tensors):
|
|
1108
|
+
def __init__(self, atoms, isotopes, tensors, total_isotropic = None):
|
|
1040
1109
|
"""
|
|
1041
1110
|
:param atoms: Tuple of atoms that this coupling is between.
|
|
1042
1111
|
:param isotopes: Tuple of the specific isotopes of atoms.
|
|
1043
1112
|
:param tensors: A dictionary of tensors.
|
|
1044
1113
|
"""
|
|
1045
|
-
super().__init__(tensors)
|
|
1114
|
+
super().__init__(tensors, total_isotropic = total_isotropic)
|
|
1046
1115
|
self.atoms = atoms
|
|
1047
1116
|
self.isotopes = isotopes
|
|
1048
1117
|
|
|
@@ -1059,7 +1128,14 @@ class NMR_spin_coupling(NMR_tensor_ABC):
|
|
|
1059
1128
|
for atom_tuple, isotopes in parser.data.nmrcouplingtensors.items():
|
|
1060
1129
|
for isotope_tuple, tensors in isotopes.items():
|
|
1061
1130
|
total_isotropic = tensors.pop("isotropic")
|
|
1062
|
-
couplings.append(
|
|
1131
|
+
couplings.append(
|
|
1132
|
+
self(
|
|
1133
|
+
(parser.results.atoms[atom_tuple[0]], parser.results.atoms[atom_tuple[1]]),
|
|
1134
|
+
isotope_tuple,
|
|
1135
|
+
tensors,
|
|
1136
|
+
total_isotropic = total_isotropic
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
1063
1139
|
|
|
1064
1140
|
except AttributeError:
|
|
1065
1141
|
return []
|
digichem/result/spectroscopy.py
CHANGED
|
@@ -420,7 +420,7 @@ class NMR_graph(Spectroscopy_graph):
|
|
|
420
420
|
For plotting an entire spectrum, use a Combined_graph of NMR_graph objects.
|
|
421
421
|
"""
|
|
422
422
|
|
|
423
|
-
def multiplicity(self, atom_group, coupling, satellite_threshold = 0.
|
|
423
|
+
def multiplicity(self, atom_group, coupling, satellite_threshold = 0.05):
|
|
424
424
|
"""
|
|
425
425
|
Determine the multiplicity of this peak.
|
|
426
426
|
|
|
@@ -456,7 +456,7 @@ class NMR_graph(Spectroscopy_graph):
|
|
|
456
456
|
# For atoms in which the majority of the abundance is already accounted for (1H, for example),
|
|
457
457
|
# exclude the residual peak.
|
|
458
458
|
residual_isotope_peaks = set(
|
|
459
|
-
[
|
|
459
|
+
[a_group for a_group, isotopes in coupling.items() if sum((a_group.element[isotope].abundance for isotope in isotopes.keys())) / 100 > satellite_threshold]
|
|
460
460
|
)
|
|
461
461
|
|
|
462
462
|
# We will keep requesting more splitting until we are able to account for all the peaks we can see.
|