gromorg 0.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.
gromorg/__init__.py ADDED
@@ -0,0 +1,309 @@
1
+ __version__ = '0.3'
2
+
3
+ from gromorg.swisparam import SwissParams
4
+ from gromorg.utils import extract_energy, extract_forces
5
+ from gromorg.capture import captured_stdout
6
+ from gromorg.data_structure import DataStructure
7
+ from gromorg.utils import commandline_operation
8
+ import gmxapi as gmx
9
+ import numpy as np
10
+ import mdtraj
11
+ import os
12
+ import shutil
13
+ import warnings
14
+
15
+
16
+ class GromOrg:
17
+ def __init__(self, structure,
18
+ params,
19
+ box=(10, 10, 10),
20
+ angles=(90, 90, 90),
21
+ supercell=(1, 1, 1),
22
+ solvent=None,
23
+ solvent_scale=0.57,
24
+ maxwarn=0,
25
+ omp_num_threads=1,
26
+ silent=False,
27
+ delete_scratch=True):
28
+
29
+ self._structure = structure
30
+ self._filename = 'test'
31
+ self._box = np.array(box)/10.0 # from Angs to nm
32
+ self._supercell = supercell
33
+ self._angles = angles
34
+ self._silent = silent
35
+ self._delete_scratch = delete_scratch
36
+ self._solvent = solvent
37
+ self._solvent_scale = solvent_scale
38
+ self._maxwarn = maxwarn
39
+
40
+ os.putenv('GMX_MAXBACKUP', '-1')
41
+ os.putenv('OMP_NUM_THREADS', '{}'.format(omp_num_threads))
42
+
43
+ folder = os.getcwd()
44
+
45
+ self._work_dir = folder + 'gromorg_{}/'.format(os.getpid())
46
+ # os.mkdir(self._work_dir)
47
+ try:
48
+ os.mkdir(self._work_dir)
49
+ except FileExistsError:
50
+ pass
51
+
52
+ self._filename_dir = self._work_dir + self._filename
53
+
54
+ # Default parameters
55
+ self._params = {# Run paramters
56
+ 'integrator': 'md-vv', # Verlet integrator
57
+ 'nsteps': 5000, # 0.001 * 5000 = 50 ps
58
+ 'dt': 0.001, # ps
59
+ # Output control
60
+ 'nstxout': 1, # save coordinates every 0.001 ps
61
+ 'nstvout': 1, # save velocities every 0.001 ps
62
+ 'nstenergy': 1, # save energies every 0.001 ps
63
+ 'nstlog': 100, # update log file every 0.1 ps
64
+ # Bond parameters
65
+ 'continuation': 'no', # first dynamics run
66
+ 'cutoff-scheme': 'Verlet', # Buffered neighbor searching
67
+ 'verlet-buffer-tolerance': 3.3e-03,
68
+ # 'ns_type': 'grid', # search neighboring grid cells
69
+ 'nstlist': 10, # 20 fs, largely irrelevant with Verlet
70
+ 'rcoulomb': 1.0, # short-range electrostatic cutoff (in nm)
71
+ 'rvdw': 1.0, # short-range van der Waals cutoff (in nm)
72
+ 'DispCorr': 'EnerPres', # account for cut-off vdW scheme
73
+ # Electrostatics
74
+ 'coulombtype': 'PME', # Particle Mesh Ewald for long-range electrostatics
75
+ 'pme_order': 4, # cubic interpolation
76
+ 'fourierspacing': 0.16, # grid spacing for FFT
77
+ # Temperature coupling is on
78
+ 'tcoupl': 'nose-hoover', # Nose-Hoover thermostat
79
+ 'tc-grps': 'system', # one coupling group
80
+ 'tau_t': 0.3, # time constant, in ps
81
+ 'ref_t': 100, # reference temperature, one for each group, in K
82
+ # Pressure coupling is off
83
+ 'pcoupl': 'no', # no pressure coupling in NVT
84
+ # Periodic boundary conditions
85
+ 'pbc': 'xyz', # 3-D PBC
86
+ # Velocity generation
87
+ 'gen_vel': 'yes', # assign velocities from Maxwell distributio
88
+ 'gen_temp': 10, # temperature for Maxwell distribution
89
+ 'gen_seed': -1, # generate a random seed
90
+ }
91
+
92
+ if params is not None:
93
+ self._params.update(params)
94
+
95
+ def get_mdp(self):
96
+ file = ';Autogenerated MDP\n'
97
+ for keys, values in self._params.items():
98
+ file += '{:30} = {}\n'.format(keys, values)
99
+
100
+ return file
101
+
102
+ def get_topology(self):
103
+
104
+ num_mol = np.prod(self._supercell)
105
+
106
+ topology_data = {'':['; Autogenerated Topology',
107
+ '#include "charmm27.ff/forcefield.itp"',
108
+ '#include "{}.itp"'.format(self._filename)],
109
+ 'system': ['molecular system name'],
110
+ 'molecules': ['{} {}\n'.format('test', num_mol)]}
111
+
112
+ return DataStructure(topology_data)
113
+
114
+ def get_tpr(self):
115
+
116
+ # write mdp file on disk
117
+ with open('{}.mdp'.format(self._filename_dir), 'w') as f:
118
+ f.write(self.get_mdp())
119
+
120
+ # get parameters for main system
121
+ sw = SwissParams(self._structure, silent=self._silent)
122
+
123
+ with open('{}.pdb'.format(self._filename_dir), 'w') as f:
124
+ f.write(sw.get_pdb_data())
125
+
126
+
127
+ # define unit cell and create gro file
128
+ if self._angles is None:
129
+
130
+ commandline_operation('gmx',
131
+ arguments=['editconf',
132
+ '-box', '{} {} {}'.format(*self._box)],
133
+ input_files={'-f': self._filename_dir + '.pdb'},
134
+ output_files={'-o': self._filename_dir + '.gro'})
135
+ else:
136
+ commandline_operation('gmx',
137
+ arguments=['editconf', #'-noc'
138
+ # '-f', self._filename_dir + '.pdb',
139
+ '-box {} {} {}'.format(*self._box),
140
+ '-bt triclinic',
141
+ '-angles {} {} {}'.format(*self._angles)],
142
+ input_files={'-f': self._filename_dir + '.pdb'},
143
+ output_files={'-o': self._filename_dir + '.gro'})
144
+
145
+ # create supercell from unitcell
146
+ commandline_operation('gmx',
147
+ arguments=['genconf',
148
+ '-nbox {} {} {}'.format(*self._supercell)],
149
+ input_files={'-f': self._filename_dir + '.gro'},
150
+ output_files={'-o': self._filename_dir + '.gro'})
151
+
152
+ # get itp and topology data
153
+ itp = DataStructure(sw.get_itp_data())
154
+ top = self.get_topology()
155
+
156
+ # Add solvent to itp and top files
157
+ if self._solvent is not None:
158
+ top, itp = self._add_solvent(top, itp)
159
+
160
+ # write topology and itp files on disk
161
+ with open('{}.top'.format(self._filename_dir), 'w') as f:
162
+ f.write(top.get_txt())
163
+
164
+ with open('{}.itp'.format(self._filename_dir), 'w') as f:
165
+ f.write(itp.get_txt())
166
+
167
+
168
+ commandline_operation('gmx',
169
+ arguments= ['grompp',
170
+ '-maxwarn {}'.format(self._maxwarn)],
171
+ input_files={'-f': self._filename_dir + '.mdp',
172
+ '-c': self._filename_dir + '.gro',
173
+ '-p': self._filename_dir + '.top',
174
+ '-po': self._filename_dir + '_log.mdp'},
175
+ output_files={'-o': self._filename_dir + '.tpr'})
176
+
177
+ tpr_data = gmx.read_tpr(self._filename_dir + '.tpr')
178
+
179
+ return tpr_data
180
+
181
+ def _add_solvent(self, top, itp):
182
+
183
+ sw_sol = SwissParams(self._solvent, silent=self._silent)
184
+
185
+ # solvent pdb
186
+ pdb_solvent = sw_sol.get_pdb_data().replace('LIG', 'SOL')
187
+
188
+ # get box that fits solvent
189
+ coords = []
190
+ for line in pdb_solvent.split('\n'):
191
+ if line.strip().startswith('ATOM'):
192
+ coords.append(line.split()[5:8])
193
+
194
+ s_box = np.max(np.array(coords, dtype=float), axis=0) - np.min(np.array(coords, dtype=float), axis=0)
195
+ s_box = np.maximum([1.0, 1.0, 1.0], s_box)
196
+
197
+ with open('{}_sol.pdb'.format(self._filename_dir), 'w') as f:
198
+ f.write(pdb_solvent)
199
+
200
+ # pdb to gro + box
201
+ commandline_operation('gmx',
202
+ arguments=['editconf',
203
+ '-box {} {} {}'.format(*s_box)],
204
+ input_files={'-f': self._filename_dir + '_sol.pdb'},
205
+ output_files={'-o': self._filename_dir + '_sol.gro'})
206
+
207
+ # get solvent itp
208
+ itp_solvent = DataStructure(sw_sol.get_itp_data().replace('LIG', 'SOL').replace('test', 'test_sol'))
209
+ itp.append_data('atomtypes', itp_solvent.get_data('atomtypes'))
210
+ itp_solvent.remove_data('')
211
+ itp_solvent.remove_data('atomtypes')
212
+ itp_solvent.remove_data('pairtypes')
213
+
214
+ # store solvent itp file
215
+ with open('{}_sol.itp'.format(self._filename_dir), 'w') as f:
216
+ f.write(itp_solvent.get_txt())
217
+
218
+ # append itp file reference to topology
219
+ top.append_data('', ['#include "{}_sol.itp"'.format(self._filename)])
220
+
221
+ open(self._filename_dir + '_sol.top', 'w').close() # temp file
222
+
223
+ # solvate system with solvent
224
+ commandline_operation('gmx',
225
+ arguments=['solvate',
226
+ '-scale {}'.format(self._solvent_scale)],
227
+ input_files={'-cp': self._filename_dir + '.gro',
228
+ '-cs': self._filename_dir + '_sol.gro',
229
+ '-p': self._filename_dir + '_sol.top'},
230
+ output_files={'-o': self._filename_dir + '.gro'})
231
+
232
+ with open(self._filename_dir + '_sol.top', 'rb') as f:
233
+ line_sol = f.read().decode('utf-8')
234
+
235
+ os.remove(self._filename_dir + '_sol.top') # delete temp file
236
+
237
+ # append solvent molecule info to topology
238
+ try:
239
+ n_solvent_mol = int(line_sol.split()[1])
240
+ top.append_data('molecules', ['test_sol {}'.format(n_solvent_mol)])
241
+
242
+ except IndexError:
243
+ warnings.warn('Solvent molecules do not fit in the supercell. '
244
+ 'Decrease the solvent size or increase solvent density using solvent_scale')
245
+
246
+ return top, itp
247
+
248
+ def run_md(self, whole=True):
249
+
250
+ md = gmx.mdrun(input=self.get_tpr())
251
+
252
+ if self._silent:
253
+ with captured_stdout(self._filename_dir + '.log'):
254
+ md.run()
255
+ else:
256
+ md.run()
257
+
258
+ trajectory_file = md.output.trajectory.result()
259
+ md_data_dir = md.output.directory.result()
260
+
261
+ if whole:
262
+ commandline_operation('gmx',
263
+ arguments=['trjconv',
264
+ '-pbc whole'],
265
+ stdin='0',
266
+ input_files={'-f': trajectory_file,
267
+ '-s': self._filename_dir + '.tpr'},
268
+ output_files={'-o': md_data_dir + '/{}.trr'.format(self._filename)})
269
+
270
+ trajectory_file = md_data_dir + '/{}.trr'.format(self._filename)
271
+
272
+ trajectory = mdtraj.load_trr(trajectory_file, top=md_data_dir + '/confout.gro')
273
+ energy = extract_energy(md_data_dir + '/ener.edr', initial=0)
274
+
275
+ if self._delete_scratch:
276
+ shutil.rmtree(md.output.directory.result())
277
+ # shutil.rmtree(self._work_dir)
278
+
279
+ return trajectory, energy
280
+
281
+ def get_forces(self):
282
+
283
+ mod_input = gmx.modify_input(input=self.get_tpr(), parameters={'nsteps': 1, 'nstfout': 1})
284
+ md = gmx.mdrun(input=mod_input)
285
+
286
+ if self._silent:
287
+ with captured_stdout(self._filename_dir + '.log'):
288
+ md.run()
289
+ else:
290
+ md.run()
291
+
292
+ trajectory_file = md.output.trajectory.result()
293
+ md_data_dir = md.output._work_dir.result()
294
+
295
+ self._trajectory = mdtraj.load_trr(trajectory_file, top=md_data_dir + '/confout.gro')
296
+ forces = extract_forces(trajectory_file, self._filename_dir + '.tpr', step=0)
297
+
298
+ if self._delete_scratch:
299
+ shutil.rmtree(md.output._work_dir.result())
300
+ # shutil.rmtree(self._work_dir)
301
+
302
+ return forces
303
+
304
+ def __del__(self):
305
+ if os.path.isdir(self._work_dir) and self._delete_scratch:
306
+ shutil.rmtree(self._work_dir)
307
+
308
+ if __name__ == '__main__':
309
+ pass
gromorg/cache.py ADDED
@@ -0,0 +1,99 @@
1
+ import sys, pickle, time, fcntl
2
+ import warnings
3
+
4
+
5
+ # Singleton class to handle cache
6
+ class SimpleCache:
7
+ __instance__ = None
8
+
9
+ def __new__(cls, *args, **kwargs):
10
+ if cls.__instance__ is not None:
11
+ return cls.__instance__
12
+
13
+ # Py2 compatibility
14
+ if sys.version_info[0] < 3:
15
+ BlockingIOError = IOError
16
+
17
+ cls._calculation_data_filename = '.parameters.pkl'
18
+ cls._pickle_protocol = pickle.HIGHEST_PROTOCOL
19
+
20
+ cls.__instance__ = super(SimpleCache, cls, ).__new__(cls)
21
+ return cls.__instance__
22
+
23
+ def __init__(self, filename=None):
24
+ """
25
+ Constructor
26
+ """
27
+
28
+ if filename is not None:
29
+ self._calculation_data_filename = filename
30
+
31
+ # python 2 compatibility
32
+ if not '_calculation_data_filename' in dir(self):
33
+ self._calculation_data_filename = '.parameters.pkl'
34
+
35
+ try:
36
+ with open(self._calculation_data_filename, 'rb') as input:
37
+ self._calculation_data = pickle.load(input)
38
+ # print('Loaded data from {}'.format(self._calculation_data_filename))
39
+ except (IOError, EOFError, BlockingIOError):
40
+ # print('Creating new calculation data file {}'.format(self._calculation_data_filename))
41
+ self._calculation_data = {}
42
+ except (UnicodeDecodeError):
43
+ warnings.warn('Warning: Calculation data file is corrupted and will be overwritten')
44
+ self._calculation_data = {}
45
+
46
+ def redefine_calculation_data_filename(self, filename):
47
+
48
+ self._calculation_data_filename = filename
49
+ # print('Set data file to {}'.format(self._calculation_data_filename))
50
+
51
+ try:
52
+ with open(self._calculation_data_filename, 'rb') as input:
53
+ self._calculation_data = pickle.load(input)
54
+ # print('Loaded data from {}'.format(self._calculation_data_filename))
55
+ except (IOError, EOFError):
56
+ # print('Creating new calculation data file {}'.format(self._calculation_data_filename))
57
+ self._calculation_data = {}
58
+
59
+ def store_calculation_data(self, structure, keyword, data, timeout=60):
60
+
61
+ for iter in range(100):
62
+ try:
63
+ with open(self._calculation_data_filename, 'rb') as input:
64
+ self._calculation_data = pickle.load(input)
65
+ except FileNotFoundError:
66
+ self._calculation_data = {}
67
+ continue
68
+ except (UnicodeDecodeError):
69
+ warnings.warn('Warning: {} file is corrupted and will be overwritten'.format(self._calculation_data_filename))
70
+ self._calculation_data = {}
71
+ except (BlockingIOError, IOError, EOFError):
72
+ # print('read_try: {}'.format(iter))
73
+ time.sleep(timeout/100)
74
+ continue
75
+ break
76
+
77
+ self._calculation_data[(hash(structure), keyword)] = data
78
+
79
+ for iter in range(100):
80
+ try:
81
+ with open(self._calculation_data_filename, 'wb') as f:
82
+ fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
83
+ pickle.dump(self._calculation_data, f, self._pickle_protocol)
84
+ except BlockingIOError:
85
+ # print('read_try: {}'.format(iter))
86
+ time.sleep(timeout/100)
87
+ continue
88
+ break
89
+
90
+ def retrieve_calculation_data(self, input_qchem, keyword):
91
+ return self._calculation_data[(hash(input_qchem), keyword)] if (hash(input_qchem), keyword) in self._calculation_data else None
92
+
93
+ @property
94
+ def calculation_data(self):
95
+ return self._calculation_data
96
+
97
+ @calculation_data.setter
98
+ def calculation_data(self, calculation_data):
99
+ self._calculation_data = calculation_data
gromorg/capture.py ADDED
@@ -0,0 +1,23 @@
1
+ import sys, os, io
2
+
3
+
4
+ class captured_stdout:
5
+ def __init__(self, filename):
6
+ self.old_stdout = None
7
+ self.fnull = None
8
+ self._filename = filename
9
+
10
+ def __enter__(self):
11
+ self.F = open(self._filename, 'w')
12
+ try:
13
+ self.old_error = os.dup(sys.stderr.fileno())
14
+ os.dup2(self.F.fileno(), sys.stderr.fileno())
15
+ except (AttributeError, io.UnsupportedOperation):
16
+ self.old_error = None
17
+ return self.F
18
+
19
+ def __exit__(self, exc_type, exc_value, traceback):
20
+ if self.old_error is not None:
21
+ os.dup2(self.old_error, sys.stderr.fileno())
22
+
23
+ self.F.close()
@@ -0,0 +1,51 @@
1
+
2
+
3
+ class DataStructure:
4
+ def __init__(self, itp_data):
5
+
6
+ if isinstance(itp_data, str):
7
+ self._data = {}
8
+ label = ''
9
+ self._data[label] = []
10
+ for lines in itp_data.split('\n'):
11
+ if len(lines.strip()) > 0: # and ';' not in lines:
12
+ if '[' in lines and ']' in lines:
13
+ label = lines.split('[')[1].split(']')[0].strip()
14
+ self._data[label] = []
15
+ continue
16
+
17
+ self._data[label].append(lines)
18
+ elif isinstance(itp_data, dict):
19
+ self._data = itp_data
20
+
21
+ else:
22
+ raise TypeError('itp_data must be str or dict')
23
+
24
+ def get_txt(self):
25
+ itp_txt = ''
26
+ for label in self._data:
27
+ if len(label) > 0:
28
+ itp_txt += '\n[ {} ]\n'.format(label)
29
+ for line in self._data[label]:
30
+ itp_txt += line + '\n'
31
+
32
+ return itp_txt
33
+
34
+ def append_line(self, label, line):
35
+ self._data[label].append(line)
36
+
37
+ def append_data(self, label, data):
38
+ self._data[label] += list(data)
39
+
40
+ def get_data(self, label):
41
+ return self._data[label]
42
+
43
+ def remove_data(self, label):
44
+ del self._data[label]
45
+
46
+
47
+ if __name__ == "__main__":
48
+ topology = DataStructure(open("../gromorg_50962/test.itp", 'r').read())
49
+ topology.append_line('atomtypes', 'CO 1 fr')
50
+ print(topology.get_txt())
51
+ print(topology.get_data('atomtypes'))
gromorg/setparam.py ADDED
@@ -0,0 +1,103 @@
1
+ import openbabel
2
+ from gromorg.cache import SimpleCache
3
+ from gromorg.utils import pdb_to_xyz
4
+ import numpy as np
5
+
6
+
7
+ class SetParams:
8
+
9
+ def __init__(self, filename='.parameters.pkl'):
10
+ self._basename = 'test'
11
+
12
+ self._cache = SimpleCache(filename=filename)
13
+
14
+ def get_hashable_connectivity(self, xyz_txt):
15
+
16
+ def sort_pairs(test):
17
+ sorted_1 = np.sort(test, axis=1)
18
+
19
+ sorted_2 = sorted(sorted_1, key=lambda x: x[1])
20
+ sorted_3 = sorted(sorted_2, key=lambda x: x[0])
21
+
22
+ return np.array(sorted_3)
23
+
24
+ class Connectivity():
25
+ def __init__(self, xyz_txt, use_types=False):
26
+
27
+ obConversion = openbabel.OBConversion()
28
+ obConversion.SetInAndOutFormats("xyz", "mol2")
29
+
30
+ mol = openbabel.OBMol()
31
+ obConversion.ReadString(mol, xyz_txt)
32
+
33
+ # mol.AddHydrogens()
34
+
35
+ atomic_data = []
36
+ for i in range(mol.NumAtoms()):
37
+ if use_types:
38
+ atomic_data.append((mol.GetAtom(i+1).GetFormalCharge(),
39
+ np.product([ord(c) for c in mol.GetAtom(i+1).GetType()]),
40
+ ))
41
+ else:
42
+ atomic_data.append((mol.GetAtom(i + 1).GetFormalCharge(),
43
+ mol.GetAtom(i + 1).GetAtomicNum()
44
+ ))
45
+
46
+ conn_index = []
47
+ for i in range(mol.NumBonds()):
48
+ conn_index.append((mol.GetBond(i).GetBeginAtomIdx()-1, mol.GetBond(i).GetEndAtomIdx()-1))
49
+
50
+ conn_index = sort_pairs(conn_index)
51
+
52
+ self._connectivity = []
53
+ for i, j in conn_index:
54
+ self._connectivity.append((atomic_data[i], atomic_data[j]))
55
+
56
+ return
57
+
58
+ def __hash__(self):
59
+ return hash(tuple(self._connectivity))
60
+
61
+ return Connectivity(xyz_txt, use_types=False)
62
+
63
+ def add_data(self, itp_file, pdb_file):
64
+
65
+ with open(itp_file, 'r') as f:
66
+ itp_txt = f.read()
67
+
68
+ with open(pdb_file, 'r') as f:
69
+ pdb_txt = f.read()
70
+
71
+ files_dict = {self._basename + '.itp': itp_txt.encode(), self._basename + '.pdb': pdb_txt.encode()}
72
+ xyz_txt = pdb_to_xyz(pdb_txt)
73
+
74
+ self._cache.store_calculation_data(self.get_hashable_connectivity(xyz_txt), 'zip_files', files_dict)
75
+
76
+ def get_data(self, structure):
77
+
78
+ xyz_txt = structure.get_xyz()
79
+ files_dict = self._cache.retrieve_calculation_data(self.get_hashable_connectivity(xyz_txt), 'zip_files')
80
+
81
+ return files_dict
82
+
83
+
84
+ if __name__ == '__main__':
85
+ from pyqchem.structure import Structure
86
+
87
+ data = SetParams(filename='.parameter.pkl')
88
+
89
+ data.add_data('test_param.itp', 'test_param.pdb')
90
+
91
+ structure = Structure(coordinates=[[ 0.6952, 0.0000, 0.0000],
92
+ [-0.6695, 0.0000, 0.0000],
93
+ [ 1.2321, 0.9289, 0.0000],
94
+ [ 1.2321, -0.9289, 0.0000],
95
+ [-1.2321, 0.9289, 0.0000],
96
+ [-1.2321, -0.9289, 0.0000]],
97
+ symbols=['C', 'C', 'H', 'H', 'H', 'H'],
98
+ charge=0,
99
+ multiplicity=1)
100
+
101
+ a = data.get_data(structure)
102
+ print(a['test.itp'])
103
+ print(a['test.pdb'])
gromorg/swisparam.py ADDED
@@ -0,0 +1,225 @@
1
+ import requests as req
2
+ import time
3
+ import io
4
+ import tarfile
5
+ from openbabel import openbabel
6
+ from gromorg.cache import SimpleCache
7
+ import numpy as np
8
+
9
+
10
+ class SwissParams:
11
+
12
+ BASE_URL = 'https://www.swissparam.ch:8443'
13
+
14
+ def __init__(self, structure, silent=False, approach='both'):
15
+ """
16
+ :param structure: molecular structure object
17
+ :param silent: suppress progress output
18
+ :param approach: parameterization approach — 'both' (default), 'mmff-based', or 'match'
19
+ """
20
+ self._structure = structure
21
+ self._filename = 'test'
22
+ self._silent = silent
23
+ self._approach = approach
24
+
25
+ self._tar_data = None
26
+ self._session_number = None
27
+
28
+ self._cache = SimpleCache()
29
+
30
+ def get_mol2(self):
31
+ obConversion = openbabel.OBConversion()
32
+ obConversion.SetInAndOutFormats("xyz", "mol2")
33
+
34
+ mol = openbabel.OBMol()
35
+ obConversion.ReadString(mol, self._structure.get_xyz())
36
+
37
+ return obConversion.WriteString(mol).replace('UNL1', 'test') # change the molname
38
+
39
+ def get_hashable_connectivity(self):
40
+
41
+ def sort_pairs(test):
42
+ sorted_1 = np.sort(test, axis=1)
43
+ sorted_2 = sorted(sorted_1, key=lambda x: x[1])
44
+ sorted_3 = sorted(sorted_2, key=lambda x: x[0])
45
+ return np.array(sorted_3)
46
+
47
+ class Connectivity():
48
+ def __init__(self, structure, use_types=False):
49
+ obConversion = openbabel.OBConversion()
50
+ obConversion.SetInAndOutFormats("xyz", "mol2")
51
+
52
+ mol = openbabel.OBMol()
53
+ obConversion.ReadString(mol, structure.get_xyz())
54
+
55
+ atomic_data = []
56
+ for i in range(mol.NumAtoms()):
57
+ if use_types:
58
+ atomic_data.append((mol.GetAtom(i+1).GetFormalCharge(),
59
+ np.product([ord(c) for c in mol.GetAtom(i+1).GetType()]),
60
+ ))
61
+ else:
62
+ atomic_data.append((mol.GetAtom(i + 1).GetFormalCharge(),
63
+ mol.GetAtom(i + 1).GetAtomicNum()
64
+ ))
65
+
66
+ conn_index = []
67
+ for i in range(mol.NumBonds()):
68
+ conn_index.append((mol.GetBond(i).GetBeginAtomIdx()-1, mol.GetBond(i).GetEndAtomIdx()-1))
69
+
70
+ conn_index = sort_pairs(conn_index)
71
+
72
+ self._connectivity = []
73
+ for i, j in conn_index:
74
+ self._connectivity.append((atomic_data[i], atomic_data[j]))
75
+
76
+ def __hash__(self):
77
+ return hash(tuple(self._connectivity))
78
+
79
+ return Connectivity(self._structure, use_types=False)
80
+
81
+ def submit_file(self):
82
+ if self._session_number is not None:
83
+ return self._session_number
84
+
85
+ url = f'{self.BASE_URL}/startparam?approach={self._approach}'
86
+
87
+ files = {'myMol2': (self._filename + '.mol2',
88
+ io.BytesIO(self.get_mol2().encode('utf-8')),
89
+ 'chemical/x-mol2')}
90
+
91
+ r = req.post(url, files=files)
92
+
93
+ if not self._silent or not r.ok:
94
+ print(f'Server response ({r.status_code}): {r.text}')
95
+
96
+ r.raise_for_status()
97
+
98
+ for line in r.text.splitlines():
99
+ if 'Session number' in line:
100
+ self._session_number = line.split(':')[1].strip()
101
+ break
102
+
103
+ if self._session_number is None:
104
+ raise Exception(f'Failed to get session number. Response:\n{r.text}')
105
+
106
+ if not self._silent:
107
+ print(f'Submitted to SwissParam. Session number: {self._session_number}')
108
+
109
+ r.close()
110
+ return self._session_number
111
+
112
+ def _check_session(self, session_number):
113
+ """Return the status text for a session."""
114
+ url = f'{self.BASE_URL}/checksession?sessionNumber={session_number}'
115
+ r = req.get(url)
116
+ r.raise_for_status()
117
+ text = r.text
118
+ r.close()
119
+ return text
120
+
121
+ def get_tar_file(self, wait_time=10):
122
+ """Poll until the job is done, then return the raw tar.gz bytes."""
123
+
124
+ if self._tar_data is not None:
125
+ return self._tar_data
126
+
127
+ session_number = self.submit_file()
128
+
129
+ if not self._silent:
130
+ print('Waiting for SwissParam...')
131
+
132
+ n = 0
133
+ while True:
134
+ status = self._check_session(session_number)
135
+
136
+ if 'Calculation is finished' in status:
137
+ break
138
+ elif 'Calculation is in the queue' in status or 'Calculation currently running' in status:
139
+ if not self._silent:
140
+ print('\b' * (np.mod(n - 1, 10) + 7), end="", flush=True)
141
+ print('waiting' + '.' * np.mod(n, 10), end="", flush=True)
142
+ n += 1
143
+ time.sleep(wait_time)
144
+ else:
145
+ raise Exception(f'Unexpected status from SwissParam:\n{status}')
146
+
147
+ # Retrieve results as tar.gz
148
+ url = f'{self.BASE_URL}/retrievesession?sessionNumber={session_number}'
149
+ r = req.get(url)
150
+ r.raise_for_status()
151
+
152
+ if not self._silent:
153
+ print('.done')
154
+
155
+ self._tar_data = r.content
156
+ r.close()
157
+
158
+ return self._tar_data
159
+
160
+ def store_param_tar(self, filename='params.tar.gz'):
161
+ with open(filename, 'wb') as f:
162
+ f.write(self.get_tar_file())
163
+
164
+ def get_data_contents(self):
165
+ files_dict = self._cache.retrieve_calculation_data(self.get_hashable_connectivity(), 'tar_files')
166
+
167
+ if files_dict is None:
168
+ raw = self.get_tar_file()
169
+
170
+ # Debug: inspect the first bytes to identify format
171
+ if not self._silent:
172
+ print(f'Response first bytes: {raw[:16]}')
173
+
174
+ tar_bytes = io.BytesIO(raw)
175
+
176
+ # Try auto-detection: handles .tar, .tar.gz, .tar.bz2
177
+ try:
178
+ with tarfile.open(fileobj=tar_bytes, mode='r:*') as tar:
179
+ files_dict = {}
180
+ for member in tar.getmembers():
181
+ f = tar.extractfile(member)
182
+ if f is not None:
183
+ name = member.name.split('/')[-1]
184
+ files_dict[name] = f.read()
185
+ except tarfile.ReadError:
186
+ # Fallback: maybe it's still a zip
187
+ from zipfile import ZipFile
188
+ tar_bytes.seek(0)
189
+ with ZipFile(tar_bytes) as zf:
190
+ files_dict = {name.split('/')[-1]: zf.read(name) for name in zf.namelist()}
191
+
192
+ self._cache.store_calculation_data(self.get_hashable_connectivity(), 'tar_files', files_dict)
193
+
194
+ return files_dict
195
+
196
+ def get_itp_data(self):
197
+ contents = self.get_data_contents()
198
+ # Find .itp file regardless of internal naming
199
+ itp_files = [k for k in contents if k.endswith('.itp')]
200
+ if not itp_files:
201
+ raise Exception('No .itp file found in SwissParam results')
202
+ return contents[itp_files[0]].decode()
203
+
204
+ def get_pdb_data(self):
205
+ contents = self.get_data_contents()
206
+ pdb_files = [k for k in contents if k.endswith('.pdb')]
207
+ if not pdb_files:
208
+ raise Exception('No .pdb file found in SwissParam results')
209
+ return contents[pdb_files[0]].decode()
210
+
211
+
212
+ if __name__ == '__main__':
213
+ from pyqchem.structure import Structure
214
+
215
+ structure = Structure(coordinates=[[ 0.6952, 0.0000, 0.0000],
216
+ [-0.6695, 0.0000, 0.0000],
217
+ [ 1.2321, 0.9289, 0.0000],
218
+ [ 1.2321,-0.9289, 0.0000],
219
+ [-1.2321, 0.9289, 0.0000],
220
+ [-1.2321,-0.9289, 0.0000]],
221
+ symbols=['C', 'C', 'H', 'H', 'H', 'H'],
222
+ charge=0,
223
+ multiplicity=1)
224
+
225
+ sp = SwissParams(structure)
gromorg/tools.py ADDED
@@ -0,0 +1,98 @@
1
+ import numpy as np
2
+ import mdtraj
3
+ from pyqchem import Structure
4
+
5
+ NM_TO_ANGSTROM = 1e1
6
+
7
+
8
+ def _label_to_element(label):
9
+ element = ''
10
+ for char in label:
11
+ if char.isnumeric():
12
+ break
13
+ element += char
14
+
15
+ return element
16
+
17
+
18
+ def mdtraj_to_pyqchem(trajectory, frame, residue, center=True):
19
+ """
20
+ Extract molecule from MDTraj trajectory in pyqhcem Structure format
21
+
22
+ :param trajectory: MDTraj trajectory
23
+ :param frame: frame index
24
+ :param residue: residue index
25
+ :param center: if true center coordinates at geometric center
26
+ :return: PyQChem structure
27
+ """
28
+
29
+ res_indices = []
30
+ names = []
31
+ for res in trajectory.topology.residues:
32
+ res_indices.append([atom.index for atom in res.atoms])
33
+ names.append([atom.name for atom in res.atoms])
34
+
35
+ # check limits
36
+ if np.abs(frame) >= trajectory.n_frames:
37
+ raise Exception('Frame error. Trajectory length is {}'.format(trajectory.n_frames))
38
+
39
+ if np.abs(residue) >= len(res_indices):
40
+ raise Exception('Residue error. Number of residues is {}'.format(len(res_indices)))
41
+
42
+
43
+ coordinates = trajectory.atom_slice(res_indices[residue])[frame].xyz[0] * NM_TO_ANGSTROM
44
+ symbols = [_label_to_element(symbol) for symbol in names[residue]]
45
+
46
+ if center:
47
+ gc_coordinates = np.average(coordinates, axis=0)
48
+ coordinates -= gc_coordinates
49
+
50
+ molecule = Structure(coordinates=coordinates,
51
+ symbols=symbols)
52
+
53
+ return molecule
54
+
55
+
56
+ def get_cluster(trajectory, frame, residue, cutoff=5.0, center=True):
57
+ """
58
+ Extract molecule cluster from a MDTraj trajectory
59
+
60
+ :param trajectory: MDTraj trajectory
61
+ :param frame: trajectory frame index
62
+ :param residue: central residue index
63
+ :param cutoff: cutoff distance in Angstroms
64
+ :param center: If true, center molecule at geometric center
65
+ :return: PyQChem structure
66
+ """
67
+
68
+ # res_indices = list(range(trajectory.topology.n_residues))
69
+
70
+ res_indices = [(residue, i) for i in range(trajectory.topology.n_residues)]
71
+ distances, pairs = mdtraj.compute_contacts(trajectory[frame], res_indices, scheme='closest', ignore_nonprotein=True)
72
+
73
+ indices = np.argwhere(distances[0] < cutoff / NM_TO_ANGSTROM).T[0]
74
+
75
+ res_indices = []
76
+ names = []
77
+ for res in trajectory.topology.residues:
78
+ res_indices.append([atom.index for atom in res.atoms])
79
+ names.append([atom.name for atom in res.atoms])
80
+
81
+ coordinates = []
82
+ symbols = []
83
+
84
+ if len(indices) == 0:
85
+ indices = [0]
86
+
87
+ for i in indices:
88
+ coordinates += list(trajectory.atom_slice(res_indices[i])[frame].xyz[0] * NM_TO_ANGSTROM)
89
+ symbols += list([_label_to_element(symbol) for symbol in names[i]])
90
+
91
+ if center:
92
+ gc_coordinates = np.average(coordinates, axis=0)
93
+ coordinates -= gc_coordinates
94
+
95
+ molecule = Structure(coordinates=coordinates,
96
+ symbols=symbols)
97
+
98
+ return molecule
gromorg/utils.py ADDED
@@ -0,0 +1,167 @@
1
+ import warnings
2
+
3
+ import numpy as np
4
+ import gmxapi as gmx
5
+ import os
6
+ from packaging import version
7
+
8
+
9
+ def commandline_operation_v1(program, arguments, stdin=None, input_files=None, output_files=None):
10
+
11
+ from subprocess import Popen, PIPE
12
+
13
+ if isinstance(arguments, list):
14
+ arguments = ' '.join(arguments)
15
+
16
+ command = '{} {}'.format(program, arguments)
17
+
18
+ for key, value in input_files.items():
19
+
20
+ if isinstance(value, list):
21
+ value = ' '.join(value)
22
+
23
+ command += ' {} {}'.format(key, value)
24
+
25
+ for key, value in output_files.items():
26
+ if isinstance(value, list):
27
+ value = ' '.join(value)
28
+
29
+ command += ' {} {}'.format(key, value)
30
+
31
+ qchem_process = Popen(command, stdout=PIPE, stdin=PIPE, stderr=PIPE, shell=True)
32
+ (output, err) = qchem_process.communicate(input=None if stdin is None else stdin.encode('utf-8'))
33
+ qchem_process.wait()
34
+ output = output.decode()
35
+ err = err.decode()
36
+
37
+ # mock class to return same output
38
+ # mock_class = type("mock_classi", (object,), {"run": lambda: None, "output":
39
+ # type("mock_class", (object,), {"erroroutput": type("mock_class", (object,), {"result": lambda: output + err}),
40
+ # "returncode": type("mock_class", (object,), {"result": lambda: 0})
41
+ # })})
42
+ # return mock_class
43
+
44
+
45
+ def commandline_operation(program, arguments, stdin=None, input_files=None, output_files=None):
46
+
47
+ if version.parse(gmx.__version__) < version.parse('0.2.0'):
48
+ warnings.warn('Using mock function for commandline_operation for back compatibility. '
49
+ 'Update to gmxapi >0.2 to get full functionally')
50
+ return commandline_operation_v1(program, arguments, stdin, input_files, output_files)
51
+
52
+ arguments_split = []
53
+ for arg in arguments:
54
+ arguments_split += arg.split()
55
+
56
+ grompp = gmx.commandline_operation(program, arguments_split,
57
+ stdin=stdin,
58
+ input_files=input_files,
59
+ output_files=output_files)
60
+
61
+ grompp.run()
62
+
63
+
64
+ if grompp.output.returncode.result() != 0:
65
+ try:
66
+ print(grompp.output.stderr.result())
67
+ except AttributeError:
68
+ pass
69
+ else:
70
+ dir_path = grompp.output.directory.result()
71
+ os.removedirs(dir_path)
72
+
73
+
74
+ def extract_energy(edr_file, output='property.xvg', initial=0, option=None):
75
+ """
76
+ 1 Bond 2 Angle 3 Proper-Dih. 4 Improper-Dih.
77
+ 5 LJ-14 6 Coulomb-14 7 LJ-(SR) 8 Disper.-corr.
78
+ 9 Coulomb-(SR) 10 Coul.-recip. 11 Potential 12 Kinetic-En.
79
+ 13 Total-Energy 14 Conserved-En. 15 Temperature 16 Pres.-DC
80
+ 17 Pressure 18 Vir-XX 19 Vir-XY 20 Vir-XZ
81
+ 21 Vir-YX 22 Vir-YY 23 Vir-YZ 24 Vir-ZX
82
+ 25 Vir-ZY 26 Vir-ZZ 27 Pres-XX 28 Pres-XY
83
+ 29 Pres-XZ 30 Pres-YX 31 Pres-YY 32 Pres-YZ
84
+ 33 Pres-ZX 34 Pres-ZY 35 Pres-ZZ 36 #Surf*SurfTen
85
+ 37 T-LIG
86
+
87
+ :param edr_file: EDR filename
88
+ :param output: output filename
89
+ :param option: option number from above
90
+ :return:
91
+ """
92
+
93
+ if option is None:
94
+ option = '11, 12, 13'
95
+
96
+ commandline_operation('gmx', ['energy'],
97
+ stdin=option,
98
+ input_files={'-f': edr_file},
99
+ output_files={'-o': edr_file + 'property.xvg'})
100
+
101
+ data = np.loadtxt(edr_file + 'property.xvg', comments=['#', '@'])[initial:].T
102
+ os.remove(edr_file + 'property.xvg')
103
+
104
+ data[1:, :] *= 0.010364272 # KJ/mol -> eV
105
+
106
+ if option == '11, 12, 13':
107
+ return {'time': list(data[0]),
108
+ 'potential': list(data[1]),
109
+ 'kinetic': list(data[2]),
110
+ 'total': list(data[3])}
111
+ else:
112
+ return data
113
+
114
+
115
+ def extract_forces(trajectory_file, tpr_file, step=500):
116
+
117
+ commandline_operation('gmx', ['traj', '-of'],
118
+ stdin='0',
119
+ input_files={'-f': trajectory_file,
120
+ '-s': tpr_file,
121
+ '-b': '{}'.format(step),
122
+ '-e': '{}'.format(step),
123
+ },
124
+ )
125
+
126
+
127
+ forces = np.loadtxt('force.xvg', comments=['#', '@'])[1:].reshape(-1, 3)
128
+ os.remove('force.xvg')
129
+
130
+ return forces * 0.00103642723 # KJ/(mol nm) to eV/ang
131
+
132
+
133
+ def pdb_to_xyz(pdb_xyz):
134
+ """
135
+ Convert a pdb file to xyz format
136
+ :param pdb_xyz:
137
+ :return:
138
+ """
139
+
140
+ def _label_to_element(label):
141
+ element = ''
142
+ for char in label:
143
+ if char.isnumeric():
144
+ break
145
+ element += char.strip()
146
+
147
+ list_changes = {'CA': 'C', 'HA': 'H'}
148
+ if element in list_changes:
149
+ element = list_changes[element]
150
+ return element
151
+
152
+ def iter_coordinates():
153
+ for line in pdb_xyz.split('\n'):
154
+ if line.startswith('ATOM'):
155
+ yield np.array(line[30:55].split(), dtype=float)
156
+
157
+ def iter_symbols():
158
+ for line in pdb_xyz.split('\n'):
159
+ if line.startswith('ATOM'):
160
+ yield _label_to_element(line[13:16])
161
+
162
+ xyz_txt = '{}\n'.format(len(list(iter_symbols())))
163
+ for s, c in zip(iter_symbols(), iter_coordinates()):
164
+ xyz_txt += '\n' + s + ' {:10.5f} {:10.5f} {:10.5f}'.format(*c)
165
+
166
+ return xyz_txt
167
+
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: gromorg
3
+ Version: 0.3
4
+ Summary: Simple Gromacs python wrapper
5
+ Home-page: https://github.com/abelcarreras/gromorg
6
+ Author: Abel Carreras
7
+ Author-email: abelcarreras83@gmail.com
8
+ Classifier: Programming Language :: Python
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy
13
+ Requires-Dist: openbabel
14
+ Requires-Dist: gmxapi
15
+ Requires-Dist: mdtraj
16
+ Requires-Dist: lxml
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license-file
24
+ Dynamic: requires-dist
25
+ Dynamic: summary
26
+
27
+ [![Documentation Status](https://readthedocs.org/projects/gromorg/badge/?version=latest)](https://gromorg.readthedocs.io/en/latest/?badge=latest)
28
+ [![PyPI version](https://badge.fury.io/py/gromorg.svg)](https://badge.fury.io/py/gromorg)
29
+
30
+
31
+ GroMorG
32
+ =======
33
+
34
+ A python tool to automate the calculation of MD simulations of small organic molecules using gromacs.
35
+ Online documentation is available at https://gromorg.readthedocs.io/
36
+
37
+ Features
38
+ --------
39
+ - Link first principles & molecular mechanics calculations using PyQchem
40
+ - Get parameters from SwissParam (https://www.swissparam.ch) automatically from molecular structure
41
+ - Clean run without intermediate files
42
+ - Add solvent molecules to the system
43
+ - Extract structures from the trajectory (including surrounding solvent molecules)
44
+
45
+ Requirements
46
+ ------------
47
+ - PyQchem (https://github.com/abelcarreras/PyQchem)
48
+ - Gromacs 2025+ (gmxapi) (http://www.gromacs.org)
49
+ - Openbabel v3.x (python API) (http://openbabel.org)
50
+ - MDtraj (https://www.mdtraj.org)
51
+
52
+ Basic example
53
+ -------------
54
+ ```python
55
+ from gromorg import GromOrg
56
+ import matplotlib.pyplot as plt
57
+ from pyqchem.structure import Structure
58
+
59
+ # Define moleule as PyQchem Structure
60
+ structure = Structure(coordinates=[[ 0.6695, 0.0000, 0.0000],
61
+ [-0.6695, 0.0000, 0.0000],
62
+ [ 1.2321, 0.9289, 0.0000],
63
+ [ 1.2321,-0.9289, 0.0000],
64
+ [-1.2321, 0.9289, 0.0000],
65
+ [-1.2321,-0.9289, 0.0000]],
66
+ symbols=['C', 'C', 'H', 'H', 'H', 'H'])
67
+
68
+ # Define Gromacs parameters
69
+ gmx_params = {
70
+ 'integrator': 'md-vv', # Verlet integrator
71
+ 'nsteps': 5000, # 0.001 * 5000 = 50 ps
72
+ 'dt': 0.001, # time step, in ps
73
+ # Temperature coupling is on
74
+ 'tcoupl': 'nose-hoover', # Nose-Hoover thermostat
75
+ 'tau_t': 0.3, # time constant, in ps
76
+ 'ref_t': 300, # reference temperature, one for each group, in K
77
+ # Bond parameters
78
+ 'gen_vel': 'yes', # assign velocities from Maxwell distributio
79
+ 'gen_temp': 300, # temperature for Maxwell distribution
80
+ 'gen_seed': -1, # generate a random seed
81
+ }
82
+
83
+ # Define simulation
84
+ calc = GromOrg(structure,
85
+ params=gmx_params, # MDP parms
86
+ box=[10, 10, 10], # a, b, c in angstrom
87
+ angles=[90, 90, 90], # alpha, beta, gamma in degree
88
+ supercell=[3, 3, 3],
89
+ delete_scratch=True, # delete temp files when finished
90
+ silent=False) # print MD log info in screen
91
+
92
+ # Run simulation and get trajectory (MDTraj) and energy
93
+ trajectory, energy = calc.run_md(whole=True)
94
+
95
+ # plot energies
96
+ plt.plot(energy['potential'], label='potential')
97
+ plt.plot(energy['kinetic'], label='kinetic')
98
+ plt.plot(energy['total'], label='total')
99
+ plt.legend()
100
+ plt.show()
101
+
102
+ # Store trajectory
103
+ trajectory.save('trajectory.gro')
104
+ ```
105
+
106
+ Contact info
107
+ ------------
108
+ Abel Carreras
109
+ abelcarreras83@gmail.com
110
+
111
+ Donostia International Physics Center (DIPC)
112
+ Donostia-San Sebastian, Euskadi (Spain)
@@ -0,0 +1,13 @@
1
+ gromorg/__init__.py,sha256=SndJPw0Wza9F_eKQ9xO8P3WeUSn2AP2zp7jsEg2ggOo,12796
2
+ gromorg/cache.py,sha256=EDTicUumhZ-SKvXhVPxE3_SLgA1wyG5jWAsZwbk_kes,3792
3
+ gromorg/capture.py,sha256=-TXe73KEDnu9SF22A9a7m8VyLa1UJcYoyxIGMVp02eM,656
4
+ gromorg/data_structure.py,sha256=DUcGUfOA8SCP12Dpd3WxOta2rsYpx1ywFdmnudSyN-A,1495
5
+ gromorg/setparam.py,sha256=8Gc3Lc1r2vtzqnlmCGJZD0It35FdGiBsr0H3h4eTUo0,3522
6
+ gromorg/swisparam.py,sha256=Q-Gv_2f-pqQ-GcLpBATk5l39sGignS4q3h4Jlo0oPoQ,8054
7
+ gromorg/tools.py,sha256=m6-LRTm_rfBNcm168Kxfnme6SyjvIprapa2n0GQwDok,2951
8
+ gromorg/utils.py,sha256=xXK_BcQXb976298mlOSMVQ2wwv55bAHkdP3dEJVjthw,5796
9
+ gromorg-0.3.dist-info/licenses/LICENSE,sha256=8WmMounu4uoRjJ0nhdKiAoWHs7IgG110IulVxkDxS4g,1086
10
+ gromorg-0.3.dist-info/METADATA,sha256=am1Ns47_WzwZLxgTtmXQC1boE-qIU_49WiSA7Nm-6ao,3949
11
+ gromorg-0.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ gromorg-0.3.dist-info/top_level.txt,sha256=Ojqsi-boL37ckmXZmlYHyoqctfIjKTW2VIj-n3SxN6g,8
13
+ gromorg-0.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Abel Carreras Conill
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ gromorg