digichem-core 6.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. digichem/__init__.py +75 -0
  2. digichem/basis.py +116 -0
  3. digichem/config/README +3 -0
  4. digichem/config/__init__.py +5 -0
  5. digichem/config/base.py +321 -0
  6. digichem/config/locations.py +14 -0
  7. digichem/config/parse.py +90 -0
  8. digichem/config/util.py +117 -0
  9. digichem/data/README +4 -0
  10. digichem/data/batoms/COPYING +18 -0
  11. digichem/data/batoms/LICENSE +674 -0
  12. digichem/data/batoms/README +2 -0
  13. digichem/data/batoms/__init__.py +0 -0
  14. digichem/data/batoms/batoms-renderer.py +351 -0
  15. digichem/data/config/digichem.yaml +714 -0
  16. digichem/data/functionals.csv +15 -0
  17. digichem/data/solvents.csv +185 -0
  18. digichem/data/tachyon/COPYING.md +5 -0
  19. digichem/data/tachyon/LICENSE +30 -0
  20. digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
  21. digichem/data/vmd/common.tcl +468 -0
  22. digichem/data/vmd/generate_combined_orbital_images.tcl +70 -0
  23. digichem/data/vmd/generate_density_images.tcl +45 -0
  24. digichem/data/vmd/generate_dipole_images.tcl +68 -0
  25. digichem/data/vmd/generate_orbital_images.tcl +57 -0
  26. digichem/data/vmd/generate_spin_images.tcl +66 -0
  27. digichem/data/vmd/generate_structure_images.tcl +40 -0
  28. digichem/datas.py +14 -0
  29. digichem/exception/__init__.py +7 -0
  30. digichem/exception/base.py +133 -0
  31. digichem/exception/uncatchable.py +63 -0
  32. digichem/file/__init__.py +1 -0
  33. digichem/file/base.py +364 -0
  34. digichem/file/cube.py +284 -0
  35. digichem/file/fchk.py +94 -0
  36. digichem/file/prattle.py +277 -0
  37. digichem/file/types.py +97 -0
  38. digichem/image/__init__.py +6 -0
  39. digichem/image/base.py +113 -0
  40. digichem/image/excited_states.py +335 -0
  41. digichem/image/graph.py +293 -0
  42. digichem/image/orbitals.py +239 -0
  43. digichem/image/render.py +617 -0
  44. digichem/image/spectroscopy.py +797 -0
  45. digichem/image/structure.py +115 -0
  46. digichem/image/vmd.py +826 -0
  47. digichem/input/__init__.py +3 -0
  48. digichem/input/base.py +78 -0
  49. digichem/input/digichem_input.py +500 -0
  50. digichem/input/gaussian.py +140 -0
  51. digichem/log.py +179 -0
  52. digichem/memory.py +166 -0
  53. digichem/misc/__init__.py +4 -0
  54. digichem/misc/argparse.py +44 -0
  55. digichem/misc/base.py +61 -0
  56. digichem/misc/io.py +239 -0
  57. digichem/misc/layered_dict.py +285 -0
  58. digichem/misc/text.py +139 -0
  59. digichem/misc/time.py +73 -0
  60. digichem/parse/__init__.py +13 -0
  61. digichem/parse/base.py +220 -0
  62. digichem/parse/cclib.py +138 -0
  63. digichem/parse/dump.py +253 -0
  64. digichem/parse/gaussian.py +130 -0
  65. digichem/parse/orca.py +96 -0
  66. digichem/parse/turbomole.py +201 -0
  67. digichem/parse/util.py +523 -0
  68. digichem/result/__init__.py +6 -0
  69. digichem/result/alignment/AA.py +114 -0
  70. digichem/result/alignment/AAA.py +61 -0
  71. digichem/result/alignment/FAP.py +148 -0
  72. digichem/result/alignment/__init__.py +3 -0
  73. digichem/result/alignment/base.py +310 -0
  74. digichem/result/angle.py +153 -0
  75. digichem/result/atom.py +742 -0
  76. digichem/result/base.py +258 -0
  77. digichem/result/dipole_moment.py +332 -0
  78. digichem/result/emission.py +402 -0
  79. digichem/result/energy.py +323 -0
  80. digichem/result/excited_state.py +821 -0
  81. digichem/result/ground_state.py +94 -0
  82. digichem/result/metadata.py +644 -0
  83. digichem/result/multi.py +98 -0
  84. digichem/result/nmr.py +1086 -0
  85. digichem/result/orbital.py +647 -0
  86. digichem/result/result.py +244 -0
  87. digichem/result/soc.py +272 -0
  88. digichem/result/spectroscopy.py +514 -0
  89. digichem/result/tdm.py +267 -0
  90. digichem/result/vibration.py +167 -0
  91. digichem/test/__init__.py +6 -0
  92. digichem/test/conftest.py +4 -0
  93. digichem/test/test_basis.py +71 -0
  94. digichem/test/test_calculate.py +30 -0
  95. digichem/test/test_config.py +78 -0
  96. digichem/test/test_cube.py +369 -0
  97. digichem/test/test_exception.py +16 -0
  98. digichem/test/test_file.py +104 -0
  99. digichem/test/test_image.py +337 -0
  100. digichem/test/test_input.py +64 -0
  101. digichem/test/test_parsing.py +79 -0
  102. digichem/test/test_prattle.py +36 -0
  103. digichem/test/test_result.py +489 -0
  104. digichem/test/test_translate.py +112 -0
  105. digichem/test/util.py +207 -0
  106. digichem/translate.py +591 -0
  107. digichem_core-6.0.0rc1.dist-info/METADATA +96 -0
  108. digichem_core-6.0.0rc1.dist-info/RECORD +111 -0
  109. digichem_core-6.0.0rc1.dist-info/WHEEL +4 -0
  110. digichem_core-6.0.0rc1.dist-info/licenses/COPYING.md +10 -0
  111. digichem_core-6.0.0rc1.dist-info/licenses/LICENSE +11 -0
@@ -0,0 +1,140 @@
1
+ from digichem.exception import Digichem_exception
2
+
3
+
4
+ class Gaussian_input_parser():
5
+ """
6
+ Class for parsing Gaussian input files.
7
+ """
8
+
9
+ # Each 'section' in Gaussian is separated by a double newline; this seems to be the only consistent delimiter.
10
+ SECTION_SEPARATOR = "\n\n"
11
+
12
+ def __init__(self, file_str = None):
13
+ """
14
+ Constructor for Gaussian input files.
15
+
16
+ :param file_str: String containing a Gaussian input file.
17
+ """
18
+ # Link 0 commands that appear at the top of the input file, we currently do no parsing on these (because we ignore them anyway)
19
+ # Link 0 commands are stored as they appear (as a string, possibly containing newlines).
20
+ self.link_0 = None
21
+
22
+ # The route section, describes the calculation to be performed.
23
+ ## Most of the route is ignored as we set it ourselves as part of the submission process, but some options (geom=connectivity, genECP etc) control the format of the rest of the file, so we do parse these.
24
+ ## This is a dictionary of options; where 'key: value' translates to 'key=value' in the gaussian file. If value is None, then the translation is to 'key'.
25
+ # The route section is currently not parsed (it is just a string).
26
+ self.route = None
27
+
28
+ # The title of the input file.
29
+ self.title = None
30
+
31
+ # The multiplicity and charge of the molecule, appears at the top of the geometry section as 'charge, mult' or 'charge mult'.
32
+ self.multiplicity = None
33
+ self.charge = None
34
+
35
+ # The geometry (atoms and charge etc) section as a string (almost certainly containing newlines).
36
+ self.geometry = None
37
+
38
+ # These are additional sections that can optionally appear in some input files (connectivity, basis set etc).
39
+ self.additional_sections = []
40
+ # If we've been given a file, load it now.
41
+ if file_str is not None:
42
+ self.load(file_str)
43
+
44
+ @property
45
+ def title(self):
46
+ """
47
+ Get the title section of this input file.
48
+
49
+ None and empty string values are translated to a single whitespace character (because otherwise they will be interpreted wrong).
50
+ """
51
+ if self._title is not None and len(self._title.trim()) != 0:
52
+ return self._title
53
+
54
+ else:
55
+ return "(title)"
56
+
57
+ @title.setter
58
+ def title(self, value):
59
+ """
60
+ Set the title section of this input file.
61
+ """
62
+ self._title = value
63
+
64
+ def load(self, file_str):
65
+ """
66
+ Load a Gaussian input file.
67
+
68
+ :param file_str: String containing a Gaussian input file.
69
+
70
+ """
71
+ # First, split on our delimeter.
72
+ sections = file_str.split("\n\n")
73
+
74
+ link_0_lines = []
75
+ route_lines = []
76
+ # Split the first section into link 0 and route (link 0 starts with %, the first line not to start without % is route).
77
+ if len(sections) > 0:
78
+ in_route = False
79
+ for line in sections[0].split("\n"):
80
+ # Check first char.
81
+ if not in_route and line[:1] != "%":
82
+ in_route = True
83
+
84
+ # Add to one of our two lists.
85
+ if not in_route:
86
+ link_0_lines.append(line)
87
+ else:
88
+ route_lines.append(line)
89
+
90
+ # Now set.
91
+ self.link_0 = "\n".join(link_0_lines) if len(link_0_lines) > 0 else None
92
+ self.route = "\n".join(route_lines) if len(route_lines) > 0 else None
93
+
94
+ # Set title.
95
+ self.title = sections[1] if len(sections) > 1 else None
96
+
97
+ # Geometry (the first line contains charge and mult).
98
+ try:
99
+ geometry_section = sections[2].split("\n", 1)
100
+ except IndexError:
101
+ # No geometry available.
102
+ raise Digichem_exception("Failed to read Gaussian geometry data from input file; is the file formatted correctly?")
103
+
104
+ # We'll first try to split on comma (,) for charge,mult.
105
+ charge_mult = geometry_section[0].split(",", 1)
106
+ # If we didn't get what we want, try on whitespace.
107
+ if len(charge_mult) != 2:
108
+ charge_mult = geometry_section[0].split(" ", 1)
109
+
110
+ # Now try and set.
111
+ try:
112
+ self.charge = int(charge_mult[0])
113
+ self.multiplicity = int(charge_mult[1])
114
+ except Exception:
115
+ raise Digichem_exception("Unable to determine charge and multiplicity from '{}'".format(charge_mult))
116
+
117
+ # The rest of the geometry section contains atoms.
118
+ if len(geometry_section) != 2 or geometry_section[1] == "":
119
+ raise Digichem_exception("Gaussian input file does not appear to contain any atoms")
120
+
121
+ self.geometry = geometry_section[1]
122
+
123
+ # Run a sanity check on the geometry format.
124
+ for geometry_line in self.geometry.split("\n"):
125
+ num_columns = len(geometry_line.split())
126
+ if num_columns > 4:
127
+ # We have too many columns?
128
+ raise Digichem_exception("Gaussian input file has too many columns ({}) in its geometry section".format(num_columns))
129
+
130
+
131
+ # And anything else.
132
+ self.additional_sections = sections[3:]
133
+
134
+ @property
135
+ def xyz(self):
136
+ """
137
+ Get the geometry of this gaussian input file in XYZ format.
138
+ """
139
+ return "{}\n\n{}".format(len(self.geometry.split("\n")), self.geometry)
140
+
digichem/log.py ADDED
@@ -0,0 +1,179 @@
1
+ import warnings
2
+ import logging.handlers
3
+ import sys
4
+ import textwrap
5
+ from subprocess import CalledProcessError
6
+
7
+ from digichem.exception import Digichem_exception
8
+
9
+ # The handler object digichem uses for logging.
10
+ LOGGING_HANDLER = None
11
+
12
+ # The name of the digichem logger, can be passed to logging.get_logger() to get the digichem logging object.
13
+ LOGGER_NAME = "digichem"
14
+
15
+
16
+ def init_logger(file_name = None, time = False):
17
+ """
18
+ Init the package wide logger.
19
+
20
+ :param file_name: Optional file to write to. If not given, messages are logged to stderr.
21
+ :param time: Whether to include the time in the logging message.
22
+ """
23
+ global LOGGING_HANDLER
24
+
25
+ logging.captureWarnings(True)
26
+
27
+ logger = logging.getLogger(LOGGER_NAME)
28
+ warnings_logger = logging.getLogger("py.warnings")
29
+
30
+ # Choose our handler.
31
+ if file_name is None:
32
+ # STDERR logging.
33
+ # The console handler, where we'll print most messages.
34
+ LOGGING_HANDLER = Handler(sys.stderr)
35
+
36
+ else:
37
+ # File logging.
38
+ LOGGING_HANDLER = logging.handlers.RotatingFileHandler(file_name, maxBytes = 1024, backupCount = 5)
39
+
40
+ # Handle everything.
41
+ LOGGING_HANDLER.setLevel(logging.DEBUG)
42
+ # Set its formatter.
43
+ var_formatter = Variable_formatter(logger, show_time = time, default_warning_formatter = warnings.formatwarning)
44
+ LOGGING_HANDLER.setFormatter(var_formatter)
45
+
46
+ # Remove old handlers.
47
+ loggers = (logger, warnings_logger)
48
+ for log in loggers:
49
+ while len(log.handlers) > 0:
50
+ log.removeHandler(log.handlers[0])
51
+
52
+ log.addHandler(LOGGING_HANDLER)
53
+
54
+ # Add the handler.
55
+ warnings.formatwarning = var_formatter.formatWarning
56
+
57
+
58
+ def set_logging_level(log_level, verbose = None):
59
+ """
60
+ Set the logging level of the digichem logger object.
61
+
62
+ :param log_level: The base logging level as a string.
63
+ :param verbose: An integer which specifies how much to increase the logging level by. If verbose is zero (or None) then the logging level is determined only by log_level.
64
+ """
65
+ logger = get_logger()
66
+
67
+ # Set from log_level first.
68
+ if log_level == "OFF":
69
+ logger.setLevel(60)
70
+ else:
71
+ logger.setLevel(log_level)
72
+
73
+ # Now adjust with verbosity.
74
+ if verbose is not None:
75
+ # Set from verbosity.
76
+ new_level = logger.level - verbose * 10
77
+
78
+ # Don't allow us to reach 0 (because this is actually 'UNSET').
79
+ if new_level <= 0:
80
+ new_level = 10
81
+
82
+ # And set.
83
+ logger.setLevel(new_level)
84
+
85
+ # # Adjust warnings if necessary.
86
+ # if logger.level == 10:
87
+ # warnings.simplefilter('always', DeprecationWarning)
88
+
89
+
90
+ def get_logger():
91
+ """
92
+ Get the logger used by all parts of digichem.
93
+ """
94
+ return logging.getLogger(LOGGER_NAME)
95
+
96
+
97
+ class Handler(logging.StreamHandler):
98
+ """
99
+ """
100
+
101
+ class Variable_formatter(logging.Formatter):
102
+ """
103
+ The logging formatter used by digichem, the format changes depending on a logger's log_level.
104
+ """
105
+
106
+ # Different formatters for printing the message.
107
+ DEFAULT_FORMATTER = '%(levelname)s: %(message)s'
108
+ WHEN_FORMATTER = '%(asctime)s: %(levelname)s: %(message)s'
109
+
110
+ # Format string for printing the date/time.
111
+ DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
112
+
113
+ def __init__(self, logger, show_time = False, *, default_warning_formatter):
114
+ super().__init__(
115
+ fmt = "digichem: " + (self.WHEN_FORMATTER if show_time else self.DEFAULT_FORMATTER),
116
+ datefmt = '%Y-%m-%d %H:%M:%S',
117
+ style = '%'
118
+ )
119
+ # Save our logger.
120
+ self.logger = logger
121
+ self.default_warning_formatter = default_warning_formatter
122
+
123
+ def formatWarning(self, message, category, filename, lineno, line=None):
124
+ """
125
+ Format a warning (from the warnings module).
126
+
127
+ :return: The formatted warning message.
128
+ """
129
+ if self.logger.getEffectiveLevel() > logging.DEBUG:
130
+ return "{}: {}".format(category.__name__, message)
131
+ else:
132
+ return self.default_warning_formatter(message, category, filename, lineno, line=None)
133
+
134
+ def formatException(self, exc_info):
135
+ """
136
+ Format an exception.
137
+
138
+ In Variable_formatter, how we do this depends on our log level.
139
+
140
+ :return: The formatted exception.
141
+ """
142
+ if isinstance(exc_info[1], Digichem_exception) and self.logger.getEffectiveLevel() > logging.DEBUG:
143
+ return self.exception_to_str(exc_info[1])
144
+ else:
145
+ return textwrap.indent(super().formatException(exc_info), " ")
146
+
147
+ @classmethod
148
+ def process_output_to_str(self, process_exception):
149
+ """
150
+ Get additional descriptive text from a CalledProcessError exception.
151
+
152
+
153
+ """
154
+ output = ""
155
+ if process_exception.stdout is not None and process_exception.stdout != "\n":
156
+ output += "\n" + process_exception.stdout.strip()
157
+ if process_exception.stderr is not None and process_exception.stderr != "\n":
158
+ output += "\n" + process_exception.stderr.strip()
159
+ return output
160
+
161
+ @classmethod
162
+ def exception_to_str(self, exception):
163
+ """
164
+ Recursively get a string representation of an exception.
165
+
166
+ :return: A string representation of exception and any prior exceptions.
167
+ """
168
+ excstr = textwrap.indent("{}: {}".format(type(exception).__name__, exception), "\t")
169
+
170
+ # If the exception is a CalledProcessError, we'll also append the stdout/stderr.
171
+ if isinstance(exception, CalledProcessError):
172
+ excstr += textwrap.indent(self.process_output_to_str(exception), "\t\t")
173
+
174
+ if exception.__cause__ is not None:
175
+ excstr += "\n{}".format(self.exception_to_str(exception.__cause__))
176
+ elif exception.__context__ is not None and not exception.__suppress_context__:
177
+ excstr += "\n{}".format(self.exception_to_str(exception.__context__))
178
+
179
+ return excstr
digichem/memory.py ADDED
@@ -0,0 +1,166 @@
1
+ import numpy
2
+
3
+
4
+ class Memory():
5
+ """
6
+ Class for storing memory-amounts.
7
+ """
8
+
9
+ # The units that we know about.
10
+ UNITS = {
11
+ 'TB': 1000000000000,
12
+ 'GB': 1000000000,
13
+ 'MB': 1000000,
14
+ 'KB': 1000,
15
+ 'B': 1
16
+ }
17
+
18
+ # When outputting units, whether to separate the number and unit with a space.
19
+ SPACE_UNIT = False
20
+
21
+ def __init__(self, value = None, print_decimal = False, round_decimal = True):
22
+ """
23
+ """
24
+ self.value = None
25
+ self.auto = value
26
+ self.print_decimal = print_decimal
27
+ self.round_decimal = round_decimal
28
+
29
+ @classmethod
30
+ def from_units(self, value, units):
31
+ """
32
+ Create a memory object from a given amount of memory and a specified unit.
33
+
34
+ :param value: The amount of memory.
35
+ :param units: The units of memory.
36
+ """
37
+ return self("{}{}".format(value, units))
38
+
39
+ @property
40
+ def auto_units(self):
41
+ """
42
+ Get the amount of memory, as a tuple, using an automatically determined suffix.
43
+ """
44
+ # First, get an ordered list from our known units.
45
+ ordered_units = sorted(self.UNITS.items(), key = lambda item: item[1])
46
+
47
+ # Now see where our number best fits (thanks numpy).
48
+ suffix_index = numpy.digitize((abs(self.value),), [ordered_unit[1] for ordered_unit in ordered_units])[0]
49
+
50
+ # Wrap if we're out of bounds and convert to proper index.
51
+ if suffix_index == 0:
52
+ suffix_index += 0
53
+ elif suffix_index == len(ordered_units):
54
+ suffix_index -= 1
55
+ else:
56
+ suffix_index -= 1
57
+
58
+ # Now check to see if we have a decimal part which is non zero.
59
+ while not self.print_decimal and suffix_index > -1 and (self.value / ordered_units[suffix_index][1]) % 1 != 0:
60
+ # Got a decimal part, use the next smallest unit.
61
+ suffix_index -= 1
62
+
63
+ if suffix_index < 0:
64
+ # We ran out of units and couldn't remove our fraction.
65
+ if self.round_decimal:
66
+ # We're allowed to round our decimal away.
67
+ suffix_index = 0
68
+ else:
69
+ raise ValueError("Unable to represent memory value '{}'B as non decimal".format(self.value))
70
+
71
+ # Now get our value in the correct units.
72
+ value = self.value / ordered_units[suffix_index][1]
73
+ if not self.print_decimal:
74
+ value = int(value)
75
+
76
+ return (value, ordered_units[suffix_index][0])
77
+
78
+ @property
79
+ def auto(self):
80
+ """
81
+ Get the amount of memory, as a string, using an automatically determined suffix.
82
+ """
83
+ value, units = self.auto_units
84
+ return "{}{}{}".format(value, " " if self.SPACE_UNIT else "", units)
85
+
86
+
87
+ @auto.setter
88
+ def auto(self, value):
89
+ """
90
+ Set the amount of memory, automatically determining the unit suffix (KB, MB, GB etc).
91
+ """
92
+ if isinstance(value, str):
93
+ # First, determine the suffix (if any).
94
+ suffix = None
95
+ for unit, amount in sorted(self.UNITS.items(), key = lambda item: item[1], reverse = True):
96
+ if value.lower().endswith(unit.lower()):
97
+ suffix = unit
98
+ # This is a bit of a hack because all suffixes contain 'B' at the end.
99
+ break
100
+
101
+ # Now remove the suffix (if there is one), convert to float and multiply by the value of suffix (to get bytes).
102
+ if suffix is not None:
103
+ value = float(value[:-len(suffix)]) * self.UNITS[suffix]
104
+
105
+ # Convert to int (because not sure it makes sense to represent fractions of bytes) and store.
106
+ self.value = int(value)
107
+
108
+ @property
109
+ def MiB(self):
110
+ """
111
+ The value of this memory in MiB.
112
+ """
113
+ return round(self.value/1048576, None)
114
+
115
+ @property
116
+ def MB(self):
117
+ """
118
+ The value of this memory in MB.
119
+ """
120
+ return round(self.value/1000000, None)
121
+
122
+ @property
123
+ def KiB(self):
124
+ """
125
+ The value of this memory in KiB.
126
+ """
127
+ return round(self.value/1024, None)
128
+
129
+ @property
130
+ def KB(self):
131
+ """
132
+ The value of this memory in KB.
133
+ """
134
+ return round(self.value/1000, None)
135
+
136
+ def __float__(self):
137
+ """
138
+ Floatify this memory amount (returning the number of bytes).
139
+ """
140
+ return float(self.value)
141
+
142
+ def __int__(self):
143
+ """
144
+ Intify this memory amount (returning the number of bytes).
145
+
146
+ In most cases, the int and float equivalents should be the same (because fractional bytes are not supported).
147
+ """
148
+ return int(self.value)
149
+
150
+ def __str__(self):
151
+ """
152
+ Stringify this memory amount (returning the number of bytes) with an appropriate suffix.
153
+ """
154
+ return self.auto
155
+
156
+ def __eq__(self, other):
157
+ return int(self) == other
158
+
159
+
160
+ class Turbomole_memory(Memory):
161
+ """
162
+ A class for representing memory quantities in formats suitable for turbomole.
163
+ """
164
+
165
+ # When outputting units, whether to separate the number and unit with a space.
166
+ SPACE_UNIT = False
@@ -0,0 +1,4 @@
1
+ from .layered_dict import Layered_dict
2
+ from .time import date_to_string
3
+ from .time import timedelta_to_string
4
+ from .io import tail, smkdir, Multi_file_wrapper, cd
@@ -0,0 +1,44 @@
1
+ import argparse
2
+
3
+
4
+ class Extend_action(argparse.Action):
5
+ """
6
+ An argparse action class to replace the "extend" action for versions of python (3.6 etc) that do not support it natively.
7
+ """
8
+
9
+ def __call__(self, parser, namespace, values, option_string = None):
10
+ """
11
+ """
12
+ try:
13
+ attr = getattr(namespace, self.dest)
14
+
15
+ except AttributeError:
16
+ attr = []
17
+ setattr(namespace, self.dest, attr)
18
+
19
+ attr.extend(values)
20
+
21
+ # TODO: Unused.
22
+ class List_grouper(argparse.Action):
23
+ """
24
+ Custom action class that groups lists together so we know in what order they were specified.
25
+ """
26
+
27
+ def __init__(self, option_strings, *args, **kwargs):
28
+ """
29
+ """
30
+ argparse.Action.__init__(self, option_strings, *args, **kwargs)
31
+ # We'll give ourself a name so we also group no matter which of our option_strings is used.
32
+ self.name = [name for name in option_strings if name[:2] == "--"]
33
+
34
+ def __call__(self, parser, namespace, values, option_string=None):
35
+ """
36
+ """
37
+ grouped_list = {
38
+ 'group': self,
39
+ 'values': values
40
+ }
41
+ try:
42
+ getattr(namespace, self.dest).append(grouped_list)
43
+ except AttributeError:
44
+ setattr(namespace, self.dest, [grouped_list])
digichem/misc/base.py ADDED
@@ -0,0 +1,61 @@
1
+ from itertools import chain, combinations
2
+
3
+ def powerset(iterable):
4
+ "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
5
+ s = list(iterable)
6
+ return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
7
+
8
+ def regular_range(median, number, spacing):
9
+ """
10
+ Generate a range of numbers with a given median and spacing.
11
+ """
12
+ if number % 2 == 0:
13
+ # Even number of peaks
14
+ peaks = []
15
+ for magnitude in range(int(number /2)):
16
+ peaks.append(median + (spacing /2 + magnitude * spacing))
17
+ peaks.append(median - (spacing /2 + magnitude * spacing))
18
+
19
+ else:
20
+ # Odd number of peaks.
21
+ peaks = [median]
22
+
23
+ for magnitude in range(int((number -1) /2)):
24
+ magnitude = magnitude+1
25
+ peaks.append(median + magnitude * spacing)
26
+ peaks.append(median - magnitude * spacing)
27
+
28
+ return sorted(peaks)
29
+
30
+
31
+ def dict_list_index(dictionary, item):
32
+ "Find the key in a dictionary which contains the list which contains an item."
33
+ for dict_key, dict_value in dictionary.items():
34
+ if item in dict_value:
35
+ return dict_key
36
+
37
+ def dict_get(dict_obj, *fields):
38
+ """
39
+ Get a value from a nested dict of arbitrary depth.
40
+
41
+ :param dict_obj: The nested dict.
42
+ :param fileds: Names of nested keys.
43
+ """
44
+ if len(fields) == 1:
45
+ return dict_obj[fields[0]]
46
+ else:
47
+ return dict_get(dict_obj[fields[0]], *fields[1:])
48
+
49
+ def transpose(nested_list, dimensions):
50
+ """
51
+ Transpose a nested list, returning separate lists of the nested items.
52
+
53
+ eg, transpose([(1, "a"), (2, "b"), (3, "c")])
54
+ -> [[1, 2, 3], ["a", "b", "c"]
55
+ """
56
+ if len(nested_list) == 0:
57
+ return [list() for i in range(dimensions)]
58
+
59
+ else:
60
+ return list(map(list, zip(*nested_list)))
61
+