digichem-core 6.1.0__py3-none-any.whl → 6.10.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. digichem/__init__.py +2 -2
  2. digichem/config/base.py +6 -4
  3. digichem/data/batoms/batoms-renderer.py +190 -50
  4. digichem/data/batoms/batoms_renderer.py +500 -0
  5. digichem/file/base.py +14 -0
  6. digichem/file/cube.py +185 -16
  7. digichem/file/types.py +1 -0
  8. digichem/image/render.py +149 -48
  9. digichem/image/vmd.py +7 -2
  10. digichem/input/digichem_input.py +2 -2
  11. digichem/memory.py +10 -0
  12. digichem/misc/io.py +95 -1
  13. digichem/parse/__init__.py +6 -1
  14. digichem/parse/base.py +85 -54
  15. digichem/parse/cclib.py +139 -13
  16. digichem/parse/dump.py +3 -3
  17. digichem/parse/orca.py +1 -0
  18. digichem/parse/pyscf.py +35 -0
  19. digichem/parse/turbomole.py +3 -3
  20. digichem/parse/util.py +146 -65
  21. digichem/result/excited_state.py +17 -11
  22. digichem/result/metadata.py +307 -3
  23. digichem/result/result.py +3 -0
  24. digichem/result/spectroscopy.py +42 -0
  25. digichem/test/conftest.py +5 -0
  26. digichem/test/mock/cubegen +87172 -0
  27. digichem/test/mock/formchk +9456 -0
  28. digichem/test/test_image.py +54 -42
  29. digichem/test/test_memory.py +33 -0
  30. digichem/test/test_parsing.py +68 -1
  31. digichem/test/test_result.py +1 -1
  32. digichem/test/util.py +4 -1
  33. {digichem_core-6.1.0.dist-info → digichem_core-6.10.3.dist-info}/METADATA +4 -3
  34. {digichem_core-6.1.0.dist-info → digichem_core-6.10.3.dist-info}/RECORD +37 -32
  35. {digichem_core-6.1.0.dist-info → digichem_core-6.10.3.dist-info}/WHEEL +1 -1
  36. {digichem_core-6.1.0.dist-info → digichem_core-6.10.3.dist-info}/licenses/COPYING.md +0 -0
  37. {digichem_core-6.1.0.dist-info → digichem_core-6.10.3.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,7 @@ from deepmerge import conservative_merger
8
8
  from pathlib import Path
9
9
  import copy
10
10
  import warnings
11
+ from scipy import integrate
11
12
 
12
13
  from digichem.misc.time import latest_datetime, total_timedelta, date_to_string,\
13
14
  timedelta_to_string
@@ -147,6 +148,7 @@ class Metadata(Result_object):
147
148
  auxiliary_files = None,
148
149
  history = None,
149
150
  date = None,
151
+ insert_date = None,
150
152
  duration = None,
151
153
  package = None,
152
154
  package_version = None,
@@ -167,6 +169,7 @@ class Metadata(Result_object):
167
169
  num_cpu = None,
168
170
  memory_available = None,
169
171
  memory_used = None,
172
+ performance = None,
170
173
 
171
174
  # Deprecated.
172
175
  solvent_model = None,
@@ -181,6 +184,7 @@ class Metadata(Result_object):
181
184
  :param history: Optional SHA of the calculation from which the coordinates of this calculation were generated.
182
185
  :param num_calculations: Optional number of individual calculations this metadata represents.
183
186
  :param date: Optional date (datetime object) of this calculation result.
187
+ :param insert_date: Optional date (datetime object) of when this calculation result was stored (normally in a DB).
184
188
  :param duration: Optional duration (timedelta object) of this calculation.
185
189
  :param package: Optional string identifying the computational chem program that performed the calculation.
186
190
  :param package_version: Optional string identifying the version of the computational chem program that performed the calculation.
@@ -206,6 +210,7 @@ class Metadata(Result_object):
206
210
  self.auxiliary_files = auxiliary_files if auxiliary_files is not None and len(auxiliary_files) != 0 else {}
207
211
  self.history = history
208
212
  self.date = date
213
+ self.insert_date = insert_date
209
214
  self.duration = duration
210
215
  self.package = package
211
216
  self.package_version = package_version
@@ -229,6 +234,7 @@ class Metadata(Result_object):
229
234
  self.num_cpu = num_cpu
230
235
  self.memory_available = memory_available
231
236
  self.memory_used = memory_used
237
+ self.performance = performance
232
238
 
233
239
  # Deprecated solvent system.
234
240
  if solvent_model is not None:
@@ -473,6 +479,7 @@ class Metadata(Result_object):
473
479
  num_cpu = parser.data.metadata.get('num_cpu', None),
474
480
  memory_available = memory_available,
475
481
  memory_used = memory_used,
482
+ performance = Performance.from_parser(parser) if "performance" in parser.data.metadata else None
476
483
  )
477
484
  except AttributeError:
478
485
  # There is no metadata available, give up.
@@ -513,6 +520,11 @@ class Metadata(Result_object):
513
520
  "units": "s",
514
521
  "string": date_to_string(self.date) if self.date is not None else None
515
522
  }
523
+ attr_dict['insert_date'] = {
524
+ "value": self.insert_date.timestamp() if self.insert_date is not None else None,
525
+ "units": "s",
526
+ "string": date_to_string(self.insert_date) if self.insert_date is not None else None
527
+ }
516
528
  attr_dict['duration'] = {
517
529
  "value": self.duration.total_seconds() if self.duration is not None else None,
518
530
  "units": "s",
@@ -542,6 +554,8 @@ class Metadata(Result_object):
542
554
  "units": None
543
555
  }
544
556
 
557
+ attr_dict['performance'] = self.performance.dump(digichem_options) if self.performance else None
558
+
545
559
  return attr_dict
546
560
 
547
561
  @classmethod
@@ -557,9 +571,14 @@ class Metadata(Result_object):
557
571
  kwargs = copy.deepcopy(data)
558
572
 
559
573
  # For more complex fields, use the data item.
560
- for attr in ['date', 'duration', 'temperature', "pressure"]:
561
- kwargs[attr] = data[attr]['value']
574
+ for attr in ['insert_date', 'date', 'duration', 'temperature', "pressure"]:
575
+ if attr in data:
576
+ kwargs[attr] = data[attr]['value']
577
+
578
+ else:
579
+ kwargs[attr] = None
562
580
 
581
+ kwargs['insert_date'] = datetime.fromtimestamp(kwargs['insert_date']) if kwargs['insert_date'] is not None else None
563
582
  kwargs['date'] = datetime.fromtimestamp(kwargs['date']) if kwargs['date'] is not None else None
564
583
  kwargs['duration'] = timedelta(seconds = kwargs['duration']) if kwargs['duration'] is not None else None
565
584
 
@@ -571,6 +590,8 @@ class Metadata(Result_object):
571
590
 
572
591
  else:
573
592
  kwargs[attr_name] = None
593
+
594
+ kwargs['performance'] = Performance.from_dump(data['performance'], result_set, options) if "performance" in data and data['performance'] is not None else None
574
595
 
575
596
  return self(**kwargs)
576
597
 
@@ -644,4 +665,287 @@ class Merged_metadata(Metadata):
644
665
  merged_metadata.auxiliary_files = conservative_merger.merge(merged_metadata.auxiliary_files, metadata.auxiliary_files)
645
666
 
646
667
  return merged_metadata
647
-
668
+
669
+ class Performance(Result_object):
670
+ """
671
+ Performance metrics and profiling data for the calculation.
672
+ """
673
+
674
+ def __init__(
675
+ self,
676
+ duration = [],
677
+ memory_used = [],
678
+ memory_used_percent = [],
679
+ memory_available = [],
680
+ memory_available_percent = [],
681
+ cpu_used = [],
682
+ output_available = [],
683
+ scratch_used = [],
684
+ scratch_available = [],
685
+ memory_allocated = None,
686
+ cpu_allocated = None,
687
+ ):
688
+ self.duration = duration
689
+ self.memory_used = memory_used
690
+ self.memory_used_percent = memory_used_percent
691
+ self.memory_available = memory_available
692
+ self.memory_available_percent = memory_available_percent
693
+ self.cpu_used = cpu_used
694
+ self.output_available = output_available
695
+ self.scratch_used = scratch_used
696
+ self.scratch_available = scratch_available
697
+
698
+ self.memory_allocated = memory_allocated if memory_allocated is not None else self.max_mem
699
+ self.cpu_allocated = cpu_allocated if cpu_allocated is not None else math.ceil(max(cpu_used) / 100)
700
+
701
+ @property
702
+ def output_space(self):
703
+ warnings.warn("output_space is deprecated, use output_available instead", DeprecationWarning)
704
+ return self.output_available
705
+
706
+ @property
707
+ def scratch_space(self):
708
+ warnings.warn("scratch_space is deprecated, use scratch_available instead", DeprecationWarning)
709
+ return self.scratch_available
710
+
711
+
712
+ @classmethod
713
+ def from_parser(self, parser):
714
+ """
715
+ Construct a Performance object from an output file parser.
716
+
717
+ :param parser: Output data parser.
718
+ :return: A populated Performance object.
719
+ """
720
+ return self(
721
+ duration = parser.data.metadata['performance']['duration'].tolist(),
722
+ memory_used = parser.data.metadata['performance']['memory_used'].tolist(),
723
+ memory_allocated = Memory(parser.data.metadata['memory_available']) if "memory_available" in parser.data.metadata else None,
724
+ memory_used_percent = parser.data.metadata['performance']['memory_used_percent'].tolist(),
725
+ memory_available = parser.data.metadata['performance']['memory_available'].tolist(),
726
+ memory_available_percent = parser.data.metadata['performance']['memory_available_percent'].tolist(),
727
+ cpu_used = parser.data.metadata['performance']['cpu_used'].tolist(),
728
+ cpu_allocated = parser.data.metadata.get('num_cpu', None),
729
+ output_available = parser.data.metadata['performance']['output_available'].tolist(),
730
+ scratch_used = parser.data.metadata['performance']['scratch_used'].tolist() if 'scratch_used' in parser.data.metadata['performance'] else [0] * len(parser.data.metadata['performance']['duration']),
731
+ scratch_available = parser.data.metadata['performance']['scratch_available'].tolist()
732
+ )
733
+
734
+
735
+ return self(
736
+ duration = parser.data.metadata['performance'][:, 0].tolist(),
737
+ memory_used = parser.data.metadata['performance'][:, 1].tolist(),
738
+ memory_allocated = Memory(parser.data.metadata['memory_available']) if "memory_available" in parser.data.metadata else None,
739
+ memory_used_percent = parser.data.metadata['performance'][:, 2].tolist(),
740
+ memory_available = parser.data.metadata['performance'][:, 3].tolist(),
741
+ memory_available_percent = parser.data.metadata['performance'][:, 4].tolist(),
742
+ cpu_used = parser.data.metadata['performance'][:, 5].tolist(),
743
+ cpu_allocated = parser.data.metadata.get('num_cpu', None),
744
+ output_space = parser.data.metadata['performance'][:, 6].tolist(),
745
+ scratch_space = parser.data.metadata['performance'][:, 7].tolist()
746
+ )
747
+
748
+ @property
749
+ def max_mem(self):
750
+ """
751
+ The maximum amount of memory used in the calculation (in bytes)
752
+ """
753
+ return max(self.memory_used)
754
+
755
+ @property
756
+ def memory_margin(self):
757
+ max_memory = float(self.memory_allocated) if self.memory_allocated is not None else self.max_mem
758
+
759
+ return max_memory - self.max_mem
760
+
761
+ @property
762
+ def memory_efficiency(self):
763
+ """
764
+ Calculate the memory efficiency of this calculation.
765
+
766
+ :param max_memory: The amount of allocated memory (in bytes), this will be guestimated automatically if not available.
767
+ """
768
+ # Integrate to find the number of byte seconds used.
769
+ area = integrate.trapezoid(self.memory_used, self.duration)
770
+
771
+ # How much we should/could have used.
772
+ total_area = (self.duration[-1] - self.duration[0]) * float(self.memory_allocated)
773
+
774
+ # Return as %.
775
+ try:
776
+ return area / total_area * 100
777
+
778
+ except Exception:
779
+ return 0
780
+
781
+ @property
782
+ def cpu_efficiency(self):
783
+ """
784
+ Calculate the CPU efficiency of this calculation.
785
+
786
+ :param max_cpu: The number of allocated CPUs, this will be guestimated automatically if not available.
787
+ """
788
+ # Integrate to find the number of CPU seconds used.
789
+ area = integrate.trapezoid(self.cpu_used, self.duration)
790
+
791
+ # How much we should/could have used.
792
+ total_area = (self.duration[-1] - self.duration[0]) * self.cpu_allocated * 100
793
+
794
+ # Return as %.
795
+ try:
796
+ return area / total_area * 100
797
+
798
+ except Exception:
799
+ # Div zero
800
+ return 0
801
+
802
+ @classmethod
803
+ def from_dump(self, data, result_set, options):
804
+ """
805
+ Get an instance of this class from its dumped representation.
806
+
807
+ :param data: The data to parse.
808
+ :param result_set: The partially constructed result set which is being populated.
809
+ """
810
+ duration = [0.0] * len(data['values'])
811
+ memory_used = [0.0] * len(data['values'])
812
+ memory_allocated = Memory(data['memory_allocated']['value'])
813
+ memory_used_percent = [0.0] * len(data['values'])
814
+ memory_available = [0.0] * len(data['values'])
815
+ memory_available_percent = [0.0] * len(data['values'])
816
+ cpu_used = [0.0] * len(data['values'])
817
+ cpu_allocated = data['cpu_allocated']
818
+ output_available = [0.0] * len(data['values'])
819
+ scratch_used = [0.0] * len(data['values'])
820
+ scratch_available = [0.0] * len(data['values'])
821
+
822
+ for i, value in enumerate(data['values']):
823
+ duration[i] = value['duration']['value']
824
+ memory_used[i] = value['memory_used']['value']
825
+ memory_used_percent[i] = value['memory_used_percent']['value']
826
+ memory_available[i] = value['memory_available']['value']
827
+ memory_available_percent[i] = value['memory_available_percent']['value']
828
+ cpu_used[i] = value['cpu_used']['value']
829
+ output_available[i] = value['output_space']['value']
830
+ if 'scratch_used' in value:
831
+ scratch_used[i] = value['scratch_used']['value']
832
+ scratch_available[i] = value['scratch_space']['value']
833
+
834
+ return self(
835
+ duration = duration,
836
+ memory_used = memory_used,
837
+ memory_allocated = memory_allocated,
838
+ memory_used_percent = memory_used_percent,
839
+ memory_available = memory_available,
840
+ memory_available_percent = memory_available_percent,
841
+ cpu_used = cpu_used,
842
+ cpu_allocated = cpu_allocated,
843
+ output_available = output_available,
844
+ scratch_used = scratch_used,
845
+ scratch_available = scratch_available
846
+ )
847
+
848
+
849
+ def dump(self, digichem_options):
850
+ """
851
+ Get a representation of this result object in primitive format.
852
+ """
853
+ return {
854
+ "cpu_allocated": self.cpu_allocated,
855
+ "cpu_efficiency": {
856
+ "units": "%",
857
+ "value": float(self.cpu_efficiency),
858
+ },
859
+ "memory_allocated": {
860
+ "units": "bytes",
861
+ "value": float(self.memory_allocated)
862
+ },
863
+ "maximum_memory": {
864
+ "units": "bytes",
865
+ "value": self.max_mem,
866
+ },
867
+ "memory_margin": {
868
+ "units": "bytes",
869
+ "value": self.memory_margin
870
+ },
871
+ "memory_efficiency": {
872
+ "units": "%",
873
+ "value": float(self.memory_efficiency)
874
+ },
875
+ "values":[
876
+ {
877
+ 'duration': {
878
+ "units": "s",
879
+ "value": self.duration[i]
880
+ },
881
+ 'memory_used': {
882
+ "units": "bytes",
883
+ "value": self.memory_used[i]
884
+ },
885
+ 'memory_used_percent': {
886
+ "units": "%",
887
+ "value": self.memory_used_percent[i]
888
+ },
889
+ 'memory_available': {
890
+ "units": "bytes",
891
+ "value": self.memory_available[i]
892
+ },
893
+ 'memory_available_percent': {
894
+ "units": "bytes",
895
+ "value": self.memory_available_percent[i]
896
+ },
897
+ 'cpu_used': {
898
+ "units": "%",
899
+ "value": self.cpu_used[i]
900
+ },
901
+ 'output_space': {
902
+ "units": "bytes",
903
+ "value": self.output_space[i]
904
+ },
905
+ 'scratch_used': {
906
+ "units": "bytes",
907
+ "value": self.scratch_used[i]
908
+ },
909
+ 'scratch_space': {
910
+ "units": "bytes",
911
+ "value": self.scratch_space[i]
912
+ }
913
+ } for i in range(len(self.duration))
914
+ ]
915
+ }
916
+
917
+ return {
918
+ 'duration': {
919
+ "units": "s",
920
+ "values": self.duration.tolist()
921
+ },
922
+ 'memory_used': {
923
+ "units": "bytes",
924
+ "values": self.memory_used.tolist()
925
+ },
926
+ 'memory_used_percent': {
927
+ "units": "%",
928
+ "values": self.memory_used_percent.tolist()
929
+ },
930
+ 'memory_available': {
931
+ "units": "bytes",
932
+ "values": self.memory_available.tolist()
933
+ },
934
+ 'memory_available_percent': {
935
+ "units": "bytes",
936
+ "values": self.memory_available_percent.tolist()
937
+ },
938
+ 'cpu_used': {
939
+ "units": "%",
940
+ "values": self.cpu_used.tolist()
941
+ },
942
+ 'output_space': {
943
+ "units": "bytes",
944
+ "values": self.output_space.tolist()
945
+ },
946
+ 'scratch_space': {
947
+ "units": "bytes",
948
+ "values": self.scratch_space.tolist()
949
+ }
950
+ }
951
+
digichem/result/result.py CHANGED
@@ -34,6 +34,9 @@ class Result_set(Result_object):
34
34
 
35
35
  self.results = (self,)
36
36
  self.emission = attributes.pop('emission', Emissions())
37
+
38
+ # Any ancillary data.
39
+ self._aux = attributes.pop('aux', {})
37
40
 
38
41
  for attr_name, attribute in attributes.items():
39
42
  setattr(self, attr_name, attribute)
@@ -333,6 +333,48 @@ class Absorption_emission_graph(Spectroscopy_graph):
333
333
  """
334
334
  return ((E * scipy.constants.electron_volt)**2 * f_E) / (scipy.constants.Planck * scipy.constants.c)
335
335
 
336
+ @classmethod
337
+ def inverse_jacobian(self, E, f_nm):
338
+ """
339
+ An implementation of the jacobian transform that scales intensity in wavelength units to intensity in energy units.
340
+
341
+ See J. Phys. Chem. Lett. 2014, 5, 20, 3497 for why this is necessary.
342
+
343
+ Note that the jacobian transform will maintain the area under the curve regardless of x units (nm or x).
344
+ Sadly, this has the consequence of mangling the intensity units (it becomes tiny; an oscillator strength of 1 at 3 eV becomes 1.163e-12).
345
+ """
346
+ # TODO: Might be better to rearrange this to accept nm rather than eV?
347
+ return (
348
+ (f_nm * scipy.constants.Planck * scipy.constants.c) / (E * scipy.constants.electron_volt) **2
349
+ )
350
+
351
+ @classmethod
352
+ def shift_coord(self, coord, delta_eV):
353
+ """
354
+ Shift a coordinate (in nm) by a given energy value.
355
+
356
+ :param delta_eV: The energy (in eV) to shift by. A positive value will blueshift (higher energy).
357
+ """
358
+ old_x_nm, old_y_nm = coord
359
+ # Convert x to energy.
360
+ old_x_ev = digichem.result.excited_state.Excited_state.wavelength_to_energy(old_x_nm)
361
+ # Transform y.
362
+ old_y_ev = self.inverse_jacobian(old_x_ev, old_y_nm)
363
+
364
+ # Shift by given amount.
365
+ new_x_ev = old_x_ev + delta_eV
366
+
367
+ # Convert back to nm.
368
+ new_x_nm, new_f_nm = self.energy_to_wavelength((new_x_ev, old_y_ev), True)
369
+
370
+ return (new_x_nm, new_f_nm)
371
+
372
+ def shift(self, delta_eV):
373
+ """
374
+ """
375
+ return map(lambda coord: self.shift_coord(coord, delta_eV), self.coordinates)
376
+
377
+
336
378
  def plot_gaussian(self):
337
379
  """
338
380
  Plot a gaussian distribution around our excited state energies.
digichem/test/conftest.py CHANGED
@@ -1,4 +1,9 @@
1
1
  import numpy
2
+ import os
3
+ from pathlib import Path
2
4
 
3
5
  # Set numpy errors (not sure why this isn't the default...)
4
6
  numpy.seterr(invalid = 'raise', divide = 'raise')
7
+
8
+ # Expand path to include mocks.
9
+ os.environ["PATH"] = str(Path(__file__).parent / "mock") + os.pathsep + os.environ["PATH"]