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.
@@ -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
+
@@ -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
+
@@ -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
- # Cache miss, go do some work.
321
+ pass
322
+
323
+ # Cache miss, go do some work.
314
324
 
315
- from rdkit.Chem import MolToSmiles
316
- from rdkit.Chem.rdmolops import RemoveHs
317
-
318
- # TODO: Find some other way of generating SMILES.
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
- mol = self.to_rdkit_molecule()
321
- try:
322
- # TODO: rdkit is unreliable, this method can fail for lots of reasons...
323
- mol = RemoveHs(mol)
324
- except Exception:
325
- pass
326
-
327
- self._smiles = MolToSmiles(mol)
328
- return self._smiles
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
- # # TODO: Handle cases where obabel isn't available
331
- # conv = Openprattle_converter.get_cls("xyz")(input_file = self.to_xyz(), input_file_type = "xyz")
332
- # # Cache the result in case we need it again.
333
- # self._smiles = conv.convert("can").strip()
334
- # return self._smiles
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 molecule; all bonds will be represented as single bonds only".format(self.formula_string)
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
 
@@ -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([atom_group.id for atom_group in atom_groups.values() if atom in atom_group.atoms][0] for atom in coupling.atoms)
572
-
573
- isotopes = coupling.isotopes
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
- group_couplings = {
611
+ average_couplings = {
586
612
  group_key: {
587
613
  isotope_key: NMR_group_spin_coupling(
588
- groups = [atom_groups[group_sub_key] for group_sub_key in group_key],
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 group_couplings.items()
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']] = (NMR_group(raw_group['group'], raw_group['shieldings'], coupling))
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 = 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
- #self.total_isotropic = total_isotropic
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
- eigenvalues = self.eigenvalues(tensor)
877
- return sum(eigenvalues) / len(eigenvalues)
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
- eigenvalues = self.eigenvalues(tensor)
914
- absolute = sum(eigenvalues) / len(eigenvalues)
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(self((parser.results.atoms[atom_tuple[0]], parser.results.atoms[atom_tuple[1]]), isotope_tuple, tensors))
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 []
@@ -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.02):
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
- [atom_group for atom_group, isotopes in coupling.items() if sum((atom_group.element[isotope].abundance for isotope in isotopes.keys())) / 100 > satellite_threshold]
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.