matmalib 0.0.1__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.
File without changes
@@ -0,0 +1,467 @@
1
+ from time import sleep
2
+
3
+ import numpy as np
4
+ import os
5
+ import sys
6
+ import networkx as nx
7
+ import re
8
+ from matmautil import *
9
+
10
+ class Atom():
11
+
12
+ def __init__(self, coord:list = None, type:str = None, valence:int = None, index:int = None):
13
+ self.coord = None
14
+ self.type = None
15
+ self.atomic_num = None
16
+ self.valence = None
17
+ self.index = None
18
+
19
+ if self.coord is not None:
20
+ self.x = self.coord[0]
21
+ self.y = self.coord[1]
22
+ self.z = self.coord[2]
23
+
24
+ def set_coord (self, coord:list):
25
+ self.coord = coord
26
+
27
+ def set_type (self, type:str):
28
+ self.type = type
29
+
30
+ def set_atomic_num (self, atomic_num:int):
31
+ self.atomic_num = atomic_num
32
+
33
+ def set_valence (self, valence:int):
34
+ self.valence = valence
35
+
36
+ def set_index (self, index:int):
37
+ self.index = index
38
+
39
+ class Mol():
40
+
41
+ def __init__(self):
42
+ self.atoms = None
43
+ self.graph = None
44
+ self.path = None
45
+ self.conn_mat = None
46
+ self.xyz
47
+
48
+ def element_symbol(A):
49
+
50
+ """ A dictionary for atomic number and atomic symbol
51
+ :param A: either atomic number or atomic symbol for Hydrogen, Carbon, Nitrogen, Oxygen, Fluorine and Silicon
52
+ :return: the corresponding atomic symbol or atomic number
53
+ """
54
+
55
+ periodic_table = {'1': 'H', '2': 'He',
56
+ '3': 'Li', '4': 'Be', '5': 'B', '6': 'C', '7': 'N', '8': 'O', '9': 'F', '10': 'Ne',
57
+ '11': 'Na', '12': 'Mg', '13': 'Al', '14': 'Si', '15': 'P', '16': 'S', '17': 'Cl', '18': 'Ar',
58
+ '19': 'K', '20': 'Ca', '35': 'Br'
59
+ }
60
+ return periodic_table[A]
61
+
62
+ def get_atoms(self):
63
+ xyz_list = self.xyz
64
+ type_list = self.type_list
65
+
66
+ atom_list = []
67
+
68
+ index = 0
69
+ for xyz, type in zip(xyz_list, type_list):
70
+ atom = Atom(coord=xyz, type=type, index=index)
71
+ atom_list.append(atom)
72
+
73
+ self.atoms = atom_list
74
+ return atom_list
75
+
76
+ def gaussian(self):
77
+
78
+ """
79
+ Parses log files from Gaussian 16 and appends information inside the log file to the mol object.
80
+
81
+ """
82
+
83
+ flags = {'freq_flag': False, 'nmr_flag': False, 'opt_flag': False, 'jcoup_flag': False, 'normal_mode': False,
84
+ 'read_geom': False}
85
+ job_type = None
86
+
87
+ # temprorary variables to hold the data
88
+ freq = [];
89
+ ints = [];
90
+ vibs = [];
91
+ geom = [];
92
+ atoms = [];
93
+ nmr = []
94
+ self.NAtoms = None
95
+
96
+ for line in open(self.path, 'r').readlines():
97
+
98
+ if not job_type and re.search('^ #', line):
99
+
100
+ if "opt" in line:
101
+ if "freq" in line:
102
+ job_type = 'optfreq'
103
+ else:
104
+ job_type = 'opt'
105
+ elif "freq" in line:
106
+ if "opt" in line:
107
+ job_type = 'optfreq'
108
+ else:
109
+ job_type = 'freq'
110
+ flags["freq_flag"] = True
111
+ elif "nmr" in line:
112
+ job_type = 'nmr'
113
+ else:
114
+ job_type = 'sp'
115
+
116
+ if self.NAtoms is None and re.search('^ NAtoms=', line):
117
+ self.NAtoms = int(line.split()[1])
118
+
119
+ if job_type == 'optfreq' or job_type == "freq":
120
+
121
+ if flags['freq_flag'] == False and re.search('Normal termination', line): flags['freq_flag'] = True
122
+ # We skip the opt part of optfreq job, all info is in the freq part
123
+
124
+ if flags['freq_flag'] == True:
125
+
126
+ if re.search('SCF Done', line):
127
+ self.E = float(line.split()[4])
128
+ elif re.search('Sum of electronic and zero-point Energies', line):
129
+ self.Ezpe = float(line.split()[6])
130
+ elif re.search('Sum of electronic and thermal Enthalpies', line):
131
+ self.H = float(line.split()[6])
132
+ elif re.search('Sum of electronic and thermal Free Energies', line):
133
+ self.F = float(line.split()[7])
134
+
135
+ elif re.search('Coordinates', line) and len(geom) == 0:
136
+ flags['read_geom'] = True
137
+
138
+ elif flags['read_geom'] == True and re.search('^\s*.\d', line):
139
+ geom.append([float(x) for x in line.split()[3:6]])
140
+ atoms.append(Mol.element_symbol(line.split()[1]))
141
+ if int(line.split()[0]) == self.NAtoms:
142
+ flags['read_geom'] = False
143
+
144
+ elif re.search(' Deg. of freedom', line):
145
+ self.NVibs = int(line.split()[3])
146
+
147
+ elif re.search('^ Frequencies', line):
148
+ freq_line = line.strip()
149
+ for f in freq_line.split()[2:5]: freq.append(float(f))
150
+ flags['normal_mode'] = False
151
+
152
+ elif re.search('^ IR Inten', line):
153
+ ir_line = line.strip()
154
+ for i in ir_line.split()[3:6]: ints.append(float(i))
155
+
156
+ elif re.search('^ Atom AN', line):
157
+ flags['normal_mode'] = True # locating normal modes of a frequency
158
+ mode_1 = [];
159
+ mode_2 = [];
160
+ mode_3 = [];
161
+ # continue
162
+
163
+ elif flags['normal_mode'] == True and re.search('^\s*\d*\s*.\d*', line) and len(line.split()) > 3:
164
+ mode_1.append([float(x) for x in line.split()[2:5]])
165
+ mode_2.append([float(x) for x in line.split()[5:8]])
166
+ mode_3.append([float(x) for x in line.split()[8:11]])
167
+
168
+ elif flags['normal_mode'] == True:
169
+ flags['normal_mode'] = False
170
+ for m in [mode_1, mode_2, mode_3]: vibs.append(np.array(m))
171
+
172
+ elif job_type == 'opt':
173
+
174
+ if re.search('SCF Done', line): E = float(line.split()[4])
175
+ if re.search('Optimization completed.', line):
176
+ self.E = E;
177
+ flags['opt_flag'] = True
178
+ if flags['opt_flag'] == True:
179
+ if re.search('Standard orientation:', line):
180
+ flags['read_geom'] = True
181
+
182
+ elif flags['read_geom'] == True and re.search('^\s*.\d', line):
183
+ geom.append([float(x) for x in line.split()[3:6]])
184
+ atoms.append(Mol.element_symbol(line.split()[1]))
185
+ if int(line.split()[0]) == self.NAtoms:
186
+ flags['read_geom'] = False
187
+
188
+ elif job_type == 'nmr':
189
+
190
+ if re.search('SCF Done', line):
191
+ self.E = float(line.split()[4])
192
+ elif re.search('Coordinates', line) and len(geom) == 0:
193
+ flags['read_geom'] = True
194
+
195
+ elif flags['read_geom'] == True and re.search('^\s*.\d', line):
196
+ geom.append([float(x) for x in line.split()[3:6]])
197
+ atoms.append(Mol.element_symbol(line.split()[1]))
198
+ if int(line.split()[0]) == self.NAtoms:
199
+ flags['read_geom'] = False
200
+
201
+ elif re.search('Total nuclear spin-spin coupling J', line):
202
+ spin = [[] for i in range(self.NAtoms)]
203
+ flags['jcoup_flag'] = True
204
+
205
+ elif flags['jcoup_flag'] == True and re.search('-?\d\.\d+[Dd][+\-]\d\d?', line):
206
+ for x in line.split()[1:]:
207
+ spin[int(line.split()[0]) - 1].append(float(x.replace('D', 'E')))
208
+
209
+ elif flags['jcoup_flag'] == True and re.search('End of Minotr F.D. properties file', line):
210
+ flags['jcoup_flag'] = False
211
+
212
+ elif job_type == 'sp':
213
+
214
+ if re.search('SCF Done', line):
215
+ self.E = float(line.split()[4])
216
+ elif re.search('Standard orientation:', line):
217
+ flags['read_geom'] = True
218
+ elif flags['read_geom'] == True and re.search('^\s*.\d', line):
219
+ geom.append([float(x) for x in line.split()[3:6]])
220
+ atoms.append(Mol.element_symbol(line.split()[1]))
221
+ if int(line.split()[0]) == self.NAtoms:
222
+ flags['read_geom'] = False
223
+
224
+ # postprocessing:
225
+ if job_type == 'freq' or job_type == 'optfreq':
226
+ self.Freq = np.array(freq)
227
+ self.Ints = np.array(ints)
228
+ self.Vibs = np.zeros((self.NVibs, self.NAtoms, 3))
229
+ for i in range(self.NVibs): self.Vibs[i, :, :] = vibs[i]
230
+
231
+ if job_type == 'nmr':
232
+ for at in spin:
233
+ while len(at) < self.NAtoms: at.append(0)
234
+ self.nmr = np.tril(spin)
235
+
236
+ self.xyz = np.array(geom)
237
+ self.type_list = atoms
238
+ self.get_atoms()
239
+
240
+ def connectivity_matrix(self, distXX=1.65, distXH=1.15):
241
+
242
+ """ Creates a connectivity matrix of the molecule. A connectivity matrix holds the information of which atoms are bonded and to what.
243
+
244
+ :param distXX: The max distance between two atoms (not hydrogen) to be considered a bond
245
+ :param distXH: The max distance between any atom and a hydrogen atom to be considered a bond
246
+ """
247
+
248
+ Nat = self.NAtoms
249
+ self.conn_mat = np.zeros((Nat, Nat))
250
+
251
+ for at1 in self.atoms:
252
+ for at2 in self.atoms:
253
+
254
+ dist = Measure.get_distance([at1,at2])
255
+
256
+ if at1 == at2:
257
+ pass
258
+
259
+ elif (at1.type == 'H' or at2.type == 'H') and dist < distXH:
260
+ self.conn_mat[at1.index, at2.index] = 1;
261
+ self.conn_mat[at2.index, at1.index] = 1
262
+ elif (at1.type != 'H' and at2.type != 'H') and dist < distXX:
263
+ self.conn_mat[at1.index, at2.index] = 1;
264
+ self.conn_mat[at2.index, at1.index] = 1
265
+
266
+ # Remove bifurcated Hs:
267
+ for at1 in self.get_atoms():
268
+ if at1.type == 'H' and np.sum(self.conn_mat[at1.index, :]) > 1:
269
+
270
+ at2list = np.where(self.conn_mat[at1.index, :] == 1)
271
+ at2list = at2list[0].tolist()
272
+
273
+ at2dist = [round(Measure.get_distance([at1, self.atoms[at2x]]), 3) for at2x in at2list]
274
+ for at, dist in zip(at2list, at2dist):
275
+ if self.atoms[at].type == 'H':
276
+ at2list.remove(at)
277
+ at2dist.remove(dist)
278
+
279
+ at2 = at2list[at2dist.index(min(at2dist))]
280
+ for at2x in at2list:
281
+ if at2x != at2:
282
+ print('remove', self._id, at2x, at1, at2)
283
+ self.conn_mat[at1.index, at2x] = 0;
284
+ self.conn_mat[at2x, at1.index] = 0
285
+
286
+ graph = nx.graph.Graph(self.conn_mat)
287
+ self.Nmols = nx.number_connected_components(graph)
288
+ self.graph = graph
289
+
290
+ def cp2k(self):
291
+
292
+ """ Parses information from CP2K. Right now only works for AIMD Trajectories, plan to implement opt compatability later.
293
+
294
+ :param job_type: (string) Currently only md is supported. Should be manually specified.
295
+ :param colvar: (list) A list containing the atom numbers that define your collective variable.
296
+ :param timestep: (float) Timestep of your MD calculation. The function will attempt to extract this from your input file if present.
297
+ :param traj_file: (string) File containing the CP2K trajectory.
298
+ :param input_file: (string) File containing your CP2K inputs.
299
+ :param output_file: (string) File containing your CP2K output.
300
+ """
301
+
302
+ with open(self.path, 'r') as geom:
303
+ first_line = geom.readline()
304
+ NAtoms = int(first_line.split()[0])
305
+
306
+ raw_coords = geom.readlines()[-NAtoms:]
307
+
308
+ coords = np.array([line.split()[1:] for line in raw_coords], dtype=float)
309
+ types = np.array([line.split()[0] for line in raw_coords])
310
+
311
+ geom.close()
312
+
313
+ self.NAtoms = NAtoms
314
+ self.xyz = coords
315
+ self.type_list = types
316
+
317
+ self.get_atoms()
318
+
319
+ def orca(self, output_file="input.log", job_type="opt"):
320
+
321
+ """ Parses information from ORCA input.log file
322
+ :param output_file: (string) File containing ORCA output file.
323
+ :param job_type: (string) The type of calculation ORCA run.
324
+ """
325
+
326
+ with open("/".join([self.path, output_file]), "r") as f:
327
+ for line in f:
328
+ if "Geometry Optimization Run" in line:
329
+ output_file = "input.orca.xyz"
330
+
331
+ with open("/".join([self.path, output_file]), "r") as f2:
332
+ first_line = f2.readline()
333
+ Natoms = int(first_line.split()[0])
334
+
335
+ second_line = f2.readlines()[0:1]
336
+ energy = np.array([i.split()[-1] for i in second_line], dtype=float)
337
+
338
+ raw_coords = f2.readlines()[-Natoms:]
339
+
340
+ coords = np.array([line.split()[1:] for line in raw_coords], dtype=float)
341
+ atoms = np.array([line.split()[0] for line in raw_coords])
342
+
343
+ self.Natoms = Natoms
344
+ self.xyz = coords
345
+ self.type_list = atoms
346
+ self.energy = energy
347
+ self.get_atoms()
348
+
349
+ elif "Single Point Calculation" and "FINAL SINGLE POINT ENERGY" in line:
350
+ # print(line.split()[-1])
351
+ job_type = "sp"
352
+ energy = np.array(line.split()[-1], dtype=float)
353
+ print(self.energy)
354
+
355
+ else:
356
+ print("Unable to read ORCA log file")
357
+
358
+ class Data ():
359
+
360
+ def __init__(self):
361
+
362
+ self.data = None
363
+
364
+ def csv(self, skiprows=1, dtype=float, delimiter: str = ','):
365
+ """
366
+ Parses a csv file and appends the data to the mol object
367
+
368
+ :param file: (string) File containing the csv data.
369
+ :param skiprows: (int) Number of rows to skip
370
+ :param dtype: (type) Data type
371
+ :param delimiter: (str) Delimiter
372
+ """
373
+
374
+ self.data = np.loadtxt(self.path, skiprows=skiprows, dtype=dtype, delimiter=delimiter)
375
+
376
+ def gromacs(self):
377
+ """ Parses information from gromacs *.xvg file
378
+
379
+ :param file: (string) File containing the gromacs data.
380
+ """
381
+
382
+ # I don't use Gromacs enough to know how the output looks like. Right now, I am just reading the .xvg file:
383
+
384
+ if re.search('.xvg', self.path):
385
+
386
+ with open(self.path, 'r') as f:
387
+ i = 0
388
+ for line in f.readlines():
389
+ if re.search('#', line) or re.search('@', line):
390
+ i = i + 1
391
+
392
+ if re.search('Time', line):
393
+ time_unit = line.split()[-1].strip('()"')
394
+ self.time_unit = time_unit
395
+ f.close()
396
+
397
+ data = np.loadtxt(self.path, skiprows=i)
398
+
399
+ self.data = data
400
+ self.software = 'gromacs'
401
+
402
+ if re.search('.xpm', self.path):
403
+
404
+ f = open(self.path, 'r')
405
+ pattern = None;
406
+ matrix_lett = [];
407
+ matrix_dict = {}
408
+ grid = None
409
+ for line in f.readlines():
410
+
411
+ if re.search(r'(\d\s\d\b)', line) and grid is None:
412
+ grid = int(line.split()[1])
413
+ pattern = grid * '[A-Z]'
414
+
415
+ if pattern and re.search(pattern, line):
416
+ replL1 = line.replace('",', '')
417
+ replL2 = line.replace('"', '')
418
+ matrix_lett.append('\n'.join(replL2.split()))
419
+
420
+ if re.search('((".*")[0-9])', line):
421
+ replL1 = line.replace('"', ' ')
422
+ letters, energy = values = str(replL1.split()[0]), float(replL1.split()[4])
423
+ matrix_dict.update(dict.fromkeys(letters, energy))
424
+
425
+ matrix = np.zeros([grid, grid])
426
+ for i in range(grid):
427
+ for j in range(grid):
428
+ matrix[grid - i - 1, j] = matrix_dict[matrix_lett[i][j]]
429
+
430
+ self.data = matrix
431
+ self.software = 'gromacs'
432
+
433
+ def cp2k(self, colvar = None, timestep = None):
434
+
435
+ self.time_unit = 'fs' # Default unit in CP2K
436
+ self.software = 'cp2k'
437
+
438
+ if re.search('.xyz', self.path):
439
+ # Reads colvars straight from the xyz file. Requires specification of timestep and colvar coordinate.
440
+
441
+ if colvar == None:
442
+ raise ValueError('Error: Specify the atoms for your collective variables.')
443
+
444
+ if timestep == None:
445
+ raise ValueError('Error: Specify timestep for reading CP2K trajectories.')
446
+
447
+ xyz_array = []
448
+
449
+ with open(self.path, 'r') as geom:
450
+ line_1 = geom.readline()
451
+ self.Natoms = int(line_1.split()[0])
452
+
453
+ for line in geom.readlines():
454
+
455
+
456
+
457
+ colvar_data = Measure.measure(self.path, colvar)
458
+ time = [i / (1 / timestep) for i in range(len(colvar_data))]
459
+
460
+ data = np.array(list(zip(time, colvar_data)))
461
+ self.data = data
462
+
463
+ elif re.search('.metadynLog', self.path):
464
+ # Reads colvars from the metadynLog file.
465
+
466
+ data = np.loadtxt(self.path)
467
+ self.data = data
@@ -0,0 +1,80 @@
1
+ import numpy as np
2
+ import os
3
+ import sys
4
+ import math
5
+
6
+ class Measure():
7
+
8
+ @staticmethod
9
+ def get_distance(atom_list):
10
+
11
+ at1 = atom_list[0]
12
+ at2 = atom_list[1]
13
+
14
+ dist = 0
15
+ for dim in range(2):
16
+ dist += (at1.coord[dim]-at2.coord[dim])**2
17
+
18
+ return math.sqrt(dist)
19
+
20
+ @staticmethod
21
+ def get_angle(atom_list):
22
+
23
+ at1 = atom_list[0]
24
+ at2 = atom_list[1]
25
+ at3 = atom_list[2]
26
+
27
+ r1 = [x-y for x,y in zip(at1.coord,at2.coord)]
28
+ r2 = [x-y for x,y in zip(at3.coord,at3.coord)]
29
+
30
+ norm1 = Measure.get_distance([at1,at2])
31
+ norm2 = Measure.get_distance([at2,at3])
32
+
33
+ norm = norm1*norm2
34
+ dot = np.dot(r1,r2)/norm
35
+ angle = math.acos(dot)
36
+ axor = np.cross(r1,r2)
37
+
38
+ return angle*180/np.pi, axor
39
+
40
+ @staticmethod
41
+ def get_dihedral(atom_list):
42
+
43
+ at1 = atom_list[0]
44
+ at2 = atom_list[1]
45
+ at3 = atom_list[2]
46
+ at4 = atom_list[3]
47
+
48
+ r1 = [x-y for x,y in zip(at1.coord,at2.coord)]
49
+ r2 = [x-y for x,y in zip(at2.coord,at3.coord)]
50
+ r3 = [x-y for x,y in zip(at3.coord,at4.coord)]
51
+
52
+ p1 = np.cross(r1,r2)
53
+ p2 = np.cross(r2,r3)
54
+
55
+ axor = np.cross(p1,p2)
56
+
57
+ norm_p1 = np.sqrt(np.sum(p1**2))
58
+ norm_p2 = np.sqrt(np.sum(p2**2))
59
+
60
+ norm = norm_p1*norm_p2
61
+ dot = np.clip(np.dot(p1,p2)/norm,-0.999999999,0.999999999)
62
+ angle = math.acos(dot)
63
+
64
+ ppoint = -np.dot(p1,at1.coord)
65
+ dpoint = np.dot(p1,at4.coord) + ppoint/norm_p1
66
+
67
+ if dpoint >= 0:
68
+ return -(angle*180/np.pi)
69
+ else:
70
+ return angle*180/np.pi
71
+
72
+ @staticmethod
73
+ def measure(atom_list):
74
+
75
+ if len(atom_list) == 2:
76
+ return Measure.get_distance(atom_list)
77
+ elif len(atom_list) == 3:
78
+ return Measure.get_angle(atom_list)
79
+ elif len(atom_list) == 4:
80
+ return Measure.get_dihedral(atom_list)
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: matmalib
3
+ Version: 0.0.1
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+
8
+ # matmalib
@@ -0,0 +1 @@
1
+ # matmalib
@@ -0,0 +1,3 @@
1
+ from .core import *
2
+ from .util import *
3
+ from .viz import *