digichem-core 7.0.4__tar.gz → 7.2.0__tar.gz

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 (123) hide show
  1. {digichem_core-7.0.4 → digichem_core-7.2.0}/PKG-INFO +2 -2
  2. {digichem_core-7.0.4 → digichem_core-7.2.0}/README.md +1 -1
  3. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/__init__.py +2 -11
  4. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/config/base.py +2 -2
  5. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/spectroscopy.py +15 -8
  6. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/input/gaussian.py +41 -1
  7. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/misc/base.py +12 -0
  8. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/base.py +11 -3
  9. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/cclib.py +2 -2
  10. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/gaussian.py +2 -2
  11. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/pyscf.py +14 -4
  12. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/atom.py +8 -0
  13. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/nmr.py +103 -27
  14. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/spectroscopy.py +2 -2
  15. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_input.py +2 -1
  16. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_result.py +2 -2
  17. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/util.py +1 -0
  18. {digichem_core-7.0.4 → digichem_core-7.2.0}/.github/workflows/python-main-pull.yml +0 -0
  19. {digichem_core-7.0.4 → digichem_core-7.2.0}/.github/workflows/python-main-push.yml +0 -0
  20. {digichem_core-7.0.4 → digichem_core-7.2.0}/.github/workflows/python-pypi-publish.yml +0 -0
  21. {digichem_core-7.0.4 → digichem_core-7.2.0}/.github/workflows/release-on-tag.yml +0 -0
  22. {digichem_core-7.0.4 → digichem_core-7.2.0}/.gitignore +0 -0
  23. {digichem_core-7.0.4 → digichem_core-7.2.0}/.gitmodules +0 -0
  24. {digichem_core-7.0.4 → digichem_core-7.2.0}/Banner.png +0 -0
  25. {digichem_core-7.0.4 → digichem_core-7.2.0}/COPYING.md +0 -0
  26. {digichem_core-7.0.4 → digichem_core-7.2.0}/LICENSE +0 -0
  27. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/basis.py +0 -0
  28. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/config/README +0 -0
  29. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/config/__init__.py +0 -0
  30. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/config/locations.py +0 -0
  31. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/config/parse.py +0 -0
  32. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/config/util.py +0 -0
  33. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/README +0 -0
  34. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/batoms/COPYING +0 -0
  35. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/batoms/LICENSE +0 -0
  36. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/batoms/README +0 -0
  37. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/batoms/__init__.py +0 -0
  38. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/batoms/batoms-renderer.py +0 -0
  39. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/batoms/batoms_renderer.py +0 -0
  40. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/config/digichem.yaml +0 -0
  41. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/functionals.csv +0 -0
  42. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/solvents.csv +0 -0
  43. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/tachyon/COPYING.md +0 -0
  44. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/tachyon/LICENSE +0 -0
  45. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
  46. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/vmd/common.tcl +0 -0
  47. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/vmd/generate_combined_orbital_images.tcl +0 -0
  48. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/vmd/generate_density_images.tcl +0 -0
  49. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/vmd/generate_dipole_images.tcl +0 -0
  50. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/vmd/generate_orbital_images.tcl +0 -0
  51. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/vmd/generate_spin_images.tcl +0 -0
  52. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/data/vmd/generate_structure_images.tcl +0 -0
  53. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/datas.py +0 -0
  54. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/exception/__init__.py +0 -0
  55. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/exception/base.py +0 -0
  56. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/exception/uncatchable.py +0 -0
  57. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/file/__init__.py +0 -0
  58. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/file/base.py +0 -0
  59. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/file/cube.py +0 -0
  60. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/file/fchk.py +0 -0
  61. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/file/prattle.py +0 -0
  62. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/file/types.py +0 -0
  63. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/__init__.py +0 -0
  64. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/base.py +0 -0
  65. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/excited_states.py +0 -0
  66. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/graph.py +0 -0
  67. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/orbitals.py +0 -0
  68. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/render.py +0 -0
  69. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/structure.py +0 -0
  70. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/image/vmd.py +0 -0
  71. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/input/__init__.py +0 -0
  72. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/input/base.py +0 -0
  73. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/input/digichem_input.py +0 -0
  74. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/log.py +0 -0
  75. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/memory.py +0 -0
  76. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/misc/__init__.py +0 -0
  77. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/misc/argparse.py +0 -0
  78. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/misc/io.py +0 -0
  79. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/misc/layered_dict.py +0 -0
  80. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/misc/text.py +0 -0
  81. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/misc/time.py +0 -0
  82. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/__init__.py +0 -0
  83. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/dump.py +0 -0
  84. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/orca.py +0 -0
  85. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/turbomole.py +0 -0
  86. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/parse/util.py +0 -0
  87. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/__init__.py +0 -0
  88. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/alignment/AA.py +0 -0
  89. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/alignment/AAA.py +0 -0
  90. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/alignment/FAP.py +0 -0
  91. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/alignment/__init__.py +0 -0
  92. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/alignment/base.py +0 -0
  93. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/angle.py +0 -0
  94. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/base.py +0 -0
  95. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/dipole_moment.py +0 -0
  96. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/emission.py +0 -0
  97. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/energy.py +0 -0
  98. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/excited_state.py +0 -0
  99. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/ground_state.py +0 -0
  100. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/metadata.py +0 -0
  101. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/multi.py +0 -0
  102. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/orbital.py +0 -0
  103. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/result.py +0 -0
  104. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/soc.py +0 -0
  105. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/tdm.py +0 -0
  106. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/result/vibration.py +0 -0
  107. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/__init__.py +0 -0
  108. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/conftest.py +0 -0
  109. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/mock/cubegen +0 -0
  110. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/mock/formchk +0 -0
  111. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_basis.py +0 -0
  112. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_calculate.py +0 -0
  113. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_config.py +0 -0
  114. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_cube.py +0 -0
  115. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_exception.py +0 -0
  116. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_file.py +0 -0
  117. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_image.py +0 -0
  118. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_memory.py +0 -0
  119. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_parsing.py +0 -0
  120. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_prattle.py +0 -0
  121. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/test/test_translate.py +0 -0
  122. {digichem_core-7.0.4 → digichem_core-7.2.0}/digichem/translate.py +0 -0
  123. {digichem_core-7.0.4 → digichem_core-7.2.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digichem-core
3
- Version: 7.0.4
3
+ Version: 7.2.0
4
4
  Summary: Open-source library for Digichem core components
5
5
  Project-URL: Homepage, https://github.com/Digichem-Project/digichem-core
6
6
  Project-URL: Documentation, https://doc.digi-chem.co.uk
@@ -52,7 +52,7 @@ Welcome to Digichem: the computational chemistry management suite!
52
52
  This is Digichem-core, the open-source library for Digichem. If you are looking to build your own computational workflows using the tools that Digichem has to offer, then you have come to the right place.
53
53
 
54
54
  - Alternatively, if you are looking for the full Digichem program, try [Build-boy](https://github.com/Digichem-Project/build-boy)
55
- - If you'd like more information on the Digichem project, check out the [website](https://www.digi-chem.co.uk)
55
+ - If you'd like more information on the Digichem project, check out the [website](https://digi-chem.co.uk)
56
56
 
57
57
  ## Documentation
58
58
 
@@ -7,7 +7,7 @@ Welcome to Digichem: the computational chemistry management suite!
7
7
  This is Digichem-core, the open-source library for Digichem. If you are looking to build your own computational workflows using the tools that Digichem has to offer, then you have come to the right place.
8
8
 
9
9
  - Alternatively, if you are looking for the full Digichem program, try [Build-boy](https://github.com/Digichem-Project/build-boy)
10
- - If you'd like more information on the Digichem project, check out the [website](https://www.digi-chem.co.uk)
10
+ - If you'd like more information on the Digichem project, check out the [website](https://digi-chem.co.uk)
11
11
 
12
12
  ## Documentation
13
13
 
@@ -11,16 +11,7 @@ from digichem.datas import get_resource
11
11
  ####################
12
12
 
13
13
 
14
- # # Version information.
15
- # major_version = 6
16
- # minor_version = 0
17
- # revision = 0
18
- # prerelease = 1
19
- # # Whether this is a development version.
20
- # development = prerelease is not None
21
- # # The full version number of this package.
22
- # __version__ = "{}.{}.{}{}".format(major_version, minor_version, revision, "-pre.{}".format(prerelease) if development else "")
23
- __version__ = "7.0.4"
14
+ __version__ = "7.2.0"
24
15
  _v_parts = __version__.split("-")[0].split(".")
25
16
  major_version = int(_v_parts[0])
26
17
  minor_version = int(_v_parts[1])
@@ -39,7 +30,7 @@ __author__ = [
39
30
  ]
40
31
 
41
32
  # Program date (when we were last updated). This is changed automatically.
42
- _last_updated_string = "18/11/2025"
33
+ _last_updated_string = "18/12/2025"
43
34
  last_updated = datetime.strptime(_last_updated_string, "%d/%m/%Y")
44
35
 
45
36
  # The sys attribute 'frozen' is our flag, '_MEIPASS' is the dir location.
@@ -249,7 +249,7 @@ To disable the maximum width, set to null.""", type = int, default = 1500),
249
249
 
250
250
  nmr = Options(help = "Options for controlling simulated NMR spectra",
251
251
  enable_rendering = Option(help = "Set to False to disable image rendering.", type = bool, default = True),
252
- coupling_filter = Option(help = "Discard J coupling that is below this threshold (in Hz)", type = float, default = 1),
252
+ coupling_filter = Option(help = "Discard J coupling that is below this threshold (in Hz)", type = float, default = 0.1),
253
253
  fwhm = Option(help = "The full-width at half-maximum; changes how wide the drawn peaks are. Note that the choice of peak width is essentially arbitrary; only the peak height is given by calculation. Units are ppm.", type = float, default = 0.01),
254
254
  y_filter = Option(help = "The minimum y value to simulate using the Gaussian function (y values below this are discarded)", type = float, default = 1e-6),
255
255
  gaussian_cutoff = Option(help = "The minimum y value to plot using the Gaussian function (controls how close to the x axis we draw the gaussian) as a fraction of the max peak height.", type = float, default = 0.001),
@@ -271,7 +271,7 @@ To disable the maximum width, set to null.""", type = int, default = 1500),
271
271
  isotopes = Option(help = "Isotope specific options. Each key should consist of a tuple of (proton_number, isotope).", type = Nested_dict_type, default = Nested_dict_type({
272
272
  # Resonance frequencies calculated at 9.3947 T.
273
273
  # 1H, increase fidelity to see more detail.
274
- "1H": {"frequency": 400, "fwhm": 0.005, "gaussian_resolution": 0.0005, "coupling_filter": 0.001, "pre_merge": 0.0005},
274
+ "1H": {"frequency": 400, "fwhm": 0.0015, "gaussian_resolution": 0.0001, "coupling_filter": 0.001, "pre_merge": 0.0005},
275
275
  # 11B.
276
276
  "11B": {"frequency": 128.3},
277
277
  # 13C.
@@ -435,7 +435,7 @@ class NMR_graph_maker_abc(Spectroscopy_graph_maker):
435
435
  label = r"$\mathdefault{{{{{}}}_{{{}}}}}$ ({})".format(atom_group.element, atom_group.index, mult_string)
436
436
  #label += "\n" + r"$\int$ = {}{}".format(len(atom_group.atoms), atom_group.element.symbol)
437
437
  #label += "\n{:.2f} ppm".format(x_coord) + r", $\int$ = {}{}".format(len(atom_group.atoms), atom_group.element.symbol)
438
- label += "\n{:.2f} ppm".format(x_coord) + r", {}{}".format(len(atom_group.atoms), atom_group.element.symbol)
438
+ label += "\n{:.2f} ppm".format(x_coord) + r", ∫ = {}{}".format(len(atom_group.atoms), atom_group.element.symbol)
439
439
 
440
440
  else:
441
441
  label = r"$\mathdefault{{{{{}}}_{{{}}}}}$".format(atom_group.element, atom_group.index)
@@ -572,9 +572,13 @@ class NMR_graph_maker(NMR_graph_maker_abc):
572
572
  # We always want to make sure that zero is shown however.
573
573
  #
574
574
  # NMR is also typically shown on a reversed scale.
575
+
576
+ # First, work out how wide our graph will be.
577
+ min_x = min(min(visible_x_values), 0)
578
+ max_x = max(max(visible_x_values), 0)
575
579
 
576
580
  x_padding = (
577
- max(visible_x_values) - min(visible_x_values)
581
+ max_x - min_x
578
582
  ) * self.x_padding_percent
579
583
 
580
584
  # If we have no negative shifts, set zero as the end of one scale.
@@ -634,7 +638,7 @@ class NMR_graph_zoom_maker(NMR_graph_maker_abc):
634
638
  self.focus = focus
635
639
 
636
640
  self.x_padding = None
637
- self.x_padding_percent = 1.0
641
+ self.x_padding_percent = 0.5
638
642
 
639
643
  self.target_width = 3.5
640
644
 
@@ -668,10 +672,11 @@ class NMR_graph_zoom_maker(NMR_graph_maker_abc):
668
672
 
669
673
  # We need to get a list of all peaks that are above our cutoff point.
670
674
  # First determine our highest point.
671
- highest_point = max(self.transpose(graph.coordinates)[1])
675
+ coords = graph.plot_cumulative_gaussian()
676
+ highest_point = max(self.transpose(coords)[1])
672
677
 
673
678
  # Now filter by a fraction of that amount.
674
- visible_x_values = [x for x, y in graph.plot_cumulative_gaussian() if y >= (highest_point * self.peak_cutoff)]
679
+ visible_x_values = [x for x, y in coords if y >= (highest_point * self.peak_cutoff)]
675
680
 
676
681
  x_padding = (
677
682
  max(visible_x_values) -min(visible_x_values)
@@ -721,9 +726,11 @@ class NMR_graph_zoom_maker(NMR_graph_maker_abc):
721
726
 
722
727
 
723
728
  highest_point = max(spectrum[1])
729
+
730
+ height = highest_point * 1.3
724
731
 
725
732
  # Clamp to 0 -> pos.
726
- self.axes.set_ylim(0, highest_point * 1.3)
733
+ self.axes.set_ylim(0 - height * 0.025, height)
727
734
 
728
735
  def plot_lines(self):
729
736
  """
@@ -796,8 +803,8 @@ class NMR_graph_zoom_maker(NMR_graph_maker_abc):
796
803
  if len(couplings) > 0 and mult[0]["number"] != 1:
797
804
  # Only show couplings for peaks we can actually distinguish.
798
805
  for (coupling_group, coupling_isotope), coupling in list(couplings.items())[:len(mult)]:
799
- #for (coupling_group, coupling_isotope), coupling in [isotope_coupling for atom_dict in couplings.values() for isotope_coupling in atom_dict.items()][:len(mult)]:
800
- label += "\n" + r"J = {:.2f} Hz ($\mathdefault{{^{{{}}}{}_{{{}}}}}$, {}{})".format(
806
+ label += "\n" + r"$\mathdefault{{^{{{}}}}}$J = {:.2f} Hz ($\mathdefault{{^{{{}}}{}_{{{}}}}}$, {}{})".format(
807
+ coupling.distance if coupling.distance is not None else "",
801
808
  coupling.total,
802
809
  coupling_isotope,
803
810
  coupling_group.element,
@@ -130,11 +130,51 @@ class Gaussian_input_parser():
130
130
 
131
131
  # And anything else.
132
132
  self.additional_sections = sections[3:]
133
+
134
+ def geometry_with_iso(self, isotopes = {}):
135
+ """
136
+ Get the geometry of this gaussian input file with specific isotopes.
137
+
138
+ :param isotopes: Isotope information. Each key should be an atomic index, or an element symbol, and each value the corresponding isotope (as an integer).
139
+ """
140
+ lines = []
141
+ for index, line in enumerate(self.geometry.split("\n")):
142
+ split_line = line.split()
143
+ atom = split_line[0]
144
+ if "(" in atom and ")" in atom:
145
+ atom = atom[atom.find("("):]
146
+
147
+ iso = None
148
+ if index in isotopes:
149
+ iso = isotopes[index]
150
+
151
+ elif atom in isotopes:
152
+ iso = isotopes[atom]
153
+
154
+ if iso is not None:
155
+ atom = "{}(iso={})".format(atom, iso)
156
+
157
+
158
+ lines.append(" ".join(
159
+ [atom, *split_line[1:]]
160
+ ))
161
+
162
+ return "\n".join(lines)
163
+
133
164
 
134
165
  @property
135
166
  def xyz(self):
136
167
  """
137
168
  Get the geometry of this gaussian input file in XYZ format.
138
169
  """
139
- return "{}\n\n{}".format(len(self.geometry.split("\n")), self.geometry)
170
+ lines = []
171
+ for line in self.geometry.split("\n"):
172
+ split_line = line.split()
173
+ atom = split_line[0]
174
+ if "(" in atom and ")" in atom:
175
+ atom = atom[atom.find("("):]
176
+ lines.append(" ".join(
177
+ [atom, *split_line[1:]]
178
+ ))
179
+ return "{}\n\n{}".format(len(self.geometry.split("\n")), "\n".join(lines))
140
180
 
@@ -1,4 +1,5 @@
1
1
  from itertools import chain, combinations
2
+ import math
2
3
 
3
4
  def powerset(iterable):
4
5
  "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
@@ -26,6 +27,17 @@ def regular_range(median, number, spacing):
26
27
  peaks.append(median - magnitude * spacing)
27
28
 
28
29
  return sorted(peaks)
30
+
31
+ def round_sig(number, sig_figs):
32
+ """Round a number to a given number of significant figures."""
33
+ try:
34
+ return round(number, sig_figs-int(math.floor(math.log10(abs(number))))-1)
35
+
36
+ except ValueError:
37
+ if number == 0:
38
+ return 0
39
+ else:
40
+ raise
29
41
 
30
42
 
31
43
  def dict_list_index(dictionary, item):
@@ -32,7 +32,7 @@ custom_parsing_formats = [
32
32
  class Parser_abc():
33
33
  """ABC for all parsers."""
34
34
 
35
- def __init__(self, *, raw_data = None, options, **kwargs):
35
+ def __init__(self, *, raw_data = None, options, metadata_defaults = None, **kwargs):
36
36
  """
37
37
  Top level constructor for calculation parsers.
38
38
  """
@@ -44,6 +44,9 @@ class Parser_abc():
44
44
 
45
45
  # Config options.
46
46
  self.options = options
47
+
48
+ # Manually provided overrides.
49
+ self.metadata_defaults = metadata_defaults if metadata_defaults is not None else {}
47
50
 
48
51
  # Parse (if we haven't already).
49
52
  try:
@@ -102,6 +105,11 @@ class Parser_abc():
102
105
  # Add current username.
103
106
  # TODO: It would probably be better if we used the name of the user who owns the output file, rather than the current user...
104
107
  self.data.metadata['user'] = self.get_current_username()
108
+
109
+ # Add any user supplied defaults.
110
+ metadata = self.metadata_defaults.copy()
111
+ metadata.update(self.data.metadata)
112
+ self.data.metadata = metadata
105
113
 
106
114
  def process_all(self):
107
115
  """
@@ -181,7 +189,7 @@ class Parser_abc():
181
189
  class File_parser_abc(Parser_abc):
182
190
  """ABC for all parsers."""
183
191
 
184
- def __init__(self, *log_files, raw_data = None, **kwargs):
192
+ def __init__(self, *log_files, raw_data = None, metadata_defaults = None, **kwargs):
185
193
  """
186
194
  Top level constructor for calculation parsers.
187
195
 
@@ -194,7 +202,7 @@ class File_parser_abc(Parser_abc):
194
202
  if len(self.log_file_paths) == 0:
195
203
  raise Digichem_exception("Cannot parse calculation output; no available log files. Are you sure the given path is a log file or directory containing log files?")
196
204
 
197
- super().__init__(raw_data=raw_data, **kwargs)
205
+ super().__init__(raw_data=raw_data, metadata_defaults = metadata_defaults, **kwargs)
198
206
 
199
207
  @classmethod
200
208
  def from_logs(self, *log_files, **kwargs):
@@ -22,7 +22,7 @@ class Cclib_parser(File_parser_abc):
22
22
  # A dictionary of recognised auxiliary file types.
23
23
  INPUT_FILE_TYPES = {}
24
24
 
25
- def __init__(self, *log_files, options, **auxiliary_files):
25
+ def __init__(self, *log_files, options, metadata_defaults = None, **auxiliary_files):
26
26
  """
27
27
  Top level constructor for calculation parsers.
28
28
 
@@ -36,7 +36,7 @@ class Cclib_parser(File_parser_abc):
36
36
  # Also have a look for a profile.csv file that we can us for performance metrics.
37
37
  self.profile_file = Path(log_files[0].parent, "../Logs/profile.csv")
38
38
 
39
- super().__init__(*log_files, options = options)
39
+ super().__init__(*log_files, options = options, metadata_defaults = metadata_defaults)
40
40
 
41
41
  @classmethod
42
42
  def from_logs(self, *log_files, hints = None, options, **kwargs):
@@ -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
  """
@@ -18,11 +18,23 @@ class Pyscf_parser(Parser_abc):
18
18
  super().__init__(**kwargs)
19
19
 
20
20
  def _parse(self):
21
+ """
22
+ Extract results from our output files.
23
+ """
21
24
  self.data = cclibfrommethods(**self.methods)
22
-
25
+
26
+ def post_parse(self):
27
+ """
28
+ Perform any required operations after line-by-line parsing.
29
+ """
30
+ super().post_parse()
31
+ # Set some metadata objects.
32
+ self.data.metadata['name'] = self.mol_name
33
+ self.data._aux = {'methods': self.methods}
34
+
23
35
  try:
24
36
  # 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()
37
+ self.data._id = hashlib.sha1(json.dumps(self.data.metadata, sort_keys = True, default = str).encode('utf-8')).hexdigest()
26
38
 
27
39
  except Exception:
28
40
  # No luck, something in metadata must be unhashable.
@@ -30,6 +42,4 @@ class Pyscf_parser(Parser_abc):
30
42
  # TODO: Think of a better way to do this.
31
43
  self.data._id = hashlib.sha1(uuid4().hex.encode('utf-8')).hexdigest()
32
44
 
33
- self.data.metadata['name'] = self.mol_name
34
- self.data._aux = {'methods': self.methods}
35
45
 
@@ -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
@@ -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.
@@ -4,7 +4,7 @@ import pytest
4
4
  from pathlib import Path
5
5
 
6
6
  from digichem.test.util import pyridine_si_v2, pyridine_si_v1, pyridine_cml,\
7
- result_files, ethane_xyz, benzene_cdx, cyclopentane_com
7
+ result_files, ethane_xyz, benzene_cdx, cyclopentane_com, cyclopentane_iso_com
8
8
  from digichem.input.digichem_input import si_from_file
9
9
 
10
10
  @pytest.mark.parametrize("file_path", [
@@ -66,6 +66,7 @@ def test_si_history(file_path, sha):
66
66
 
67
67
  @pytest.mark.parametrize("file_path, format", [
68
68
  (cyclopentane_com, "mol"),
69
+ (cyclopentane_iso_com, "mol"),
69
70
  (ethane_xyz, "cml"),
70
71
  (benzene_cdx, "xyz"),
71
72
  (result_files['gaussian'][0], "com")
@@ -465,8 +465,8 @@ def test_nmr_h_isotope_options(orca_nmr_result):
465
465
  # Check we have the right options set for H.
466
466
  options = orca_nmr_result.nmr.spectrometer.isotope_options(1, 1)
467
467
  assert options['frequency'] == 400
468
- assert options['fwhm'] == 0.005
469
- assert options['gaussian_resolution'] == 0.0005
468
+ assert options['fwhm'] == 0.0015
469
+ assert options['gaussian_resolution'] == 0.0001
470
470
  assert options['coupling_filter'] == 0.001
471
471
  assert options['pre_merge'] == 0.0005
472
472
 
@@ -18,6 +18,7 @@ def data_directory():
18
18
  benzene_cdx = Path(data_directory(), "Input", "Benzene.cdx")
19
19
  pyridine_cml = Path(data_directory(), "Input", "Pyridine.cml")
20
20
  cyclopentane_com = Path(data_directory(), "Input", "Cyclopentane.com")
21
+ cyclopentane_iso_com = Path(data_directory(), "Input", "Cyclopentane_iso.com")
21
22
  water_xyz = Path(data_directory(), "Input", "Water.xyz")
22
23
  ethane_xyz = Path(data_directory(), "Input", "Ethane.xyz")
23
24
  pyridine_si_v2 = Path(data_directory(), "Input/Pyridine.v2.si")
File without changes
File without changes
File without changes
File without changes
File without changes