ben-chem-tools 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.3
2
+ Name: ben-chem-tools
3
+ Version: 0.1.0
4
+ Summary: common useful tools for computational chemistry
5
+ Author: Ben
6
+ Author-email: Ben <bennyp2494@gmail.com>
7
+ Requires-Dist: cclib>=1.8.1
8
+ Requires-Dist: matplotlib>=3.9.4
9
+ Requires-Dist: numpy>=2.0.2
10
+ Requires-Dist: pandas>=2.3.3
11
+ Requires-Dist: periodictable>=2.0.2
12
+ Requires-Dist: scipy>=1.13.1
13
+ Requires-Dist: seaborn>=0.13.2
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+
File without changes
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "ben-chem-tools"
3
+ version = "0.1.0"
4
+ description = "common useful tools for computational chemistry"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Ben", email = "bennyp2494@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "cclib>=1.8.1",
12
+ "matplotlib>=3.9.4",
13
+ "numpy>=2.0.2",
14
+ "pandas>=2.3.3",
15
+ "periodictable>=2.0.2",
16
+ "scipy>=1.13.1",
17
+ "seaborn>=0.13.2",
18
+ ]
19
+
20
+ [project.scripts]
21
+ ben-chem-tools = "ben_chem_tools:main"
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.9.18,<0.10.0"]
25
+ build-backend = "uv_build"
@@ -0,0 +1,8 @@
1
+ from .geometries import xyz_atom
2
+ from .geometries import xyz_molecule
3
+
4
+ from .input_creation import g16_input
5
+ from .input_creation import stationary_point_count
6
+ from .input_creation import get_n_geometry
7
+ from .input_creation import rerun_from_last_geom
8
+ from .input_creation import quick_rerun_optimizations
@@ -0,0 +1,137 @@
1
+ import numpy as np
2
+ import periodictable as pt
3
+
4
+ ##################################################
5
+ # This file holds objects for geometries like
6
+ # atoms and molecules.
7
+ ##################################################
8
+
9
+
10
+ class xyz_atom:
11
+
12
+ def __init__(self,atom_label:str,x_coord:float,y_coord:float,z_coord:float):
13
+
14
+ self.atom_label = str(atom_label).split()[0]
15
+
16
+ try:
17
+ self.x_coord = float(x_coord)
18
+ except ValueError:
19
+ raise ValueError("x_coord must be a numeric value.")
20
+ try:
21
+ self.y_coord = float(x_coord)
22
+ except ValueError:
23
+ raise ValueError("y_coord must be a numeric value.")
24
+ try:
25
+ self.z_coord = float(x_coord)
26
+ except ValueError:
27
+ raise ValueError("z_coord must be a numeric value.")
28
+
29
+ @classmethod
30
+ def from_string(cls,atom_string:str):
31
+ atom_string = str(atom_string).strip().split()
32
+ if len(atom_string) !=4:
33
+ raise ValueError(f"Supplied string splits in to {len(atom_string)} items. Method requires 4.")
34
+ return cls(*atom_string)
35
+
36
+ @classmethod
37
+ def from_cclib_vals(cls,atom_num:int,coords:list[float]):
38
+ try:
39
+ atom_label = pt.elements[int(atom_num)]
40
+ except ValueError:
41
+ atom_label = atom_num
42
+ except KeyError:
43
+ KeyError(f"atom number {atom_num} not found")
44
+ if len(coords) != 3:
45
+ raise ValueError(f"coords is {len(coords)} long. It must have a length of 3.")
46
+ return cls(atom_label,coords[0],coords[1],coords[2])
47
+
48
+ def as_list(self):
49
+ return [self.atom_label,[self.x_coord,self.y_coord,self.z_coord]]
50
+
51
+ def apply_transormation(self,transformation_matrix,inplace=False):
52
+
53
+ transformation_matrix = np.array(transformation_matrix)
54
+ transformation_matrix = transformation_matrix.reshape((3,3))
55
+ position_vector = np.array([self.x_coord,self.y_coord,self.z_coord])
56
+ position_vector = position_vector @ transformation_matrix
57
+
58
+ if inplace:
59
+ self.x_coord = float(position_vector[0])
60
+ self.y_coord = float(position_vector[1])
61
+ self.z_coord = float(position_vector[2])
62
+ return position_vector
63
+
64
+ def __str__(self):
65
+ return f"{self.atom_label} {self.x_coord:.6f} {self.y_coord:.6f} {self.z_coord:.6f}"
66
+
67
+ def __repr__(self):
68
+ return f"{self.atom_label} {self.x_coord:.6f} {self.y_coord:.6f} {self.z_coord:.6f}"
69
+
70
+
71
+
72
+
73
+ class xyz_molecule:
74
+
75
+ def __init__(self,atom_list:list[xyz_atom]):
76
+ self.atom_list:list[xyz_atom] = atom_list
77
+
78
+ @classmethod
79
+ def from_cclib_vals(cls,atom_nums,atom_coords):
80
+ acc = []
81
+ for atom_num,atom_coord in zip(atom_nums,atom_coords):
82
+ acc.append(xyz_atom.from_cclib_vals(atom_num,atom_coords))
83
+ return cls(acc)
84
+
85
+ @classmethod
86
+ def from_string(cls,molecule_string:str):
87
+ molecule_string = molecule_string.strip()
88
+ acc = []
89
+ for atom_string in molecule_string.split("\n"):
90
+ acc.append(xyz_atom.from_string(atom_string))
91
+ return cls(acc)
92
+
93
+ @classmethod
94
+ def from_xyz_file(cls,file_path:str):
95
+ with open(file_path,"r") as file:
96
+ file_lines = file.readlines()
97
+ num_atoms = int(file_lines[0].strip())
98
+ acc = []
99
+ for line in file_lines[2:2+num_atoms]:
100
+ acc.append(xyz_atom.from_string(line))
101
+ return cls(acc)
102
+
103
+ def add_atom(self,new_atom):
104
+ self.atom_list.append(new_atom)
105
+
106
+
107
+
108
+ def as_list(self):
109
+ return [atom.as_list() for atom in self.atom_list]
110
+
111
+ def write_xyz_file(self,file_name,comment_line=""):
112
+ with open(file_name,"w") as outfile:
113
+ outfile.write(f"{len(self.atom_list)}\n")
114
+ outfile.write(f"{comment_line}\n")
115
+ for atom in self.atom_list:
116
+ outfile.write(f"{str(atom)}\n")
117
+
118
+ def apply_transformation(self,transformation_matrix,inplace=False):
119
+ transformed_coords = []
120
+ for atom in self.atom_list:
121
+ transformed_coords.append(atom.apply_transormation(transformation_matrix,inplace))
122
+ return transformed_coords
123
+
124
+ def __str__(self):
125
+ acc = ""
126
+ for atom in self.atom_list:
127
+ acc += f"{str(atom)}\n"
128
+ acc = acc.strip()
129
+ return acc
130
+
131
+ def __repr__(self):
132
+ acc = ""
133
+ for atom in self.atom_list:
134
+ acc += f"{str(atom)}\n"
135
+ acc = acc.strip()
136
+ return acc
137
+
@@ -0,0 +1,173 @@
1
+ from glob import glob
2
+ import cclib
3
+ import periodictable as pt
4
+ from geometries import xyz_molecule, xyz_atom
5
+ import os
6
+
7
+
8
+ class g16_input:
9
+ """ a base level g16 input file
10
+ """
11
+
12
+ def __init__(self, input_line:str, geometry:xyz_molecule, file_name:str,charge:int,spin_mult:int, nproc = 36,mem = 5):
13
+ self.checkpoint = file_name[:-3] + "chk"
14
+ self.file_name = file_name
15
+ self.geometry = geometry
16
+ self.mem = mem
17
+ self.nproc = nproc
18
+ self.input_line = input_line.lower()
19
+ self.extra = ""
20
+ self.title_card = "Title Card"
21
+ self.charge = charge
22
+ self.spin_mult = spin_mult
23
+
24
+
25
+ def write_file(self):
26
+ """writes a file associated with the g16 input instance
27
+ """
28
+ out_string = f"""%chk={self.checkpoint}
29
+ %nproc={self.nproc}
30
+ %mem={self.mem}GB
31
+ #p {self.input_line}
32
+
33
+ {self.title_card}
34
+
35
+ {self.charge} {self.spin_mult}
36
+ {str(self.geometry)}
37
+
38
+ {self.extra}
39
+
40
+
41
+ """
42
+ with open(self.file_name,"w") as file:
43
+ file.write(out_string)
44
+
45
+ @classmethod
46
+ def from_file(cls,file_name):
47
+ collect_input = False
48
+ input_line = ""
49
+ nproc = 36
50
+ mem = 5
51
+ charge_collected = False
52
+ charge = 0
53
+ spin_mult = 1
54
+ Title_Card = False
55
+ blank_counter = 0
56
+ geometry = xyz_molecule([])
57
+
58
+ with open(file_name,"r") as file:
59
+ for line in file:
60
+ true_line = line.strip(" \n")
61
+
62
+ if "%mem" in true_line:
63
+ mem = int(true_line.split("=")[1][:-2])
64
+
65
+ if "%nproc" in true_line:
66
+ nproc = int(true_line.split("=")[1])
67
+
68
+ if collect_input == True and blank_counter == 0:
69
+ input_line = input_line + " " + line.strip(" \n")
70
+
71
+ if "#p" in true_line:
72
+ collect_input = True
73
+ input_line = input_line + line.strip("\n")
74
+
75
+ if blank_counter == 2 and charge_collected and true_line != "":
76
+ geometry.add_atom(xyz_atom.from_string(true_line))
77
+
78
+ if blank_counter == 2 and charge_collected == False:
79
+ true_line = line.strip(" \n\t")
80
+ charge_mult = true_line.split()
81
+ charge = charge_mult[0]
82
+ spin_mult = charge_mult[1]
83
+ charge_collected = True
84
+
85
+ if true_line.strip("\t") == "":
86
+ blank_counter = blank_counter + 1
87
+
88
+
89
+
90
+ return cls(input_line,geometry,file_name,charge,spin_mult,nproc = nproc,mem = mem)
91
+
92
+
93
+ def stationary_point_count(output_file_pattern:str="*.log"):
94
+ """Evaluates files for number of stationary points
95
+ Parameters
96
+ ----------
97
+ output_file_pattern (str): a file pattern to match and evaluate stationary points.
98
+
99
+ Returns
100
+ -------
101
+ list[tuple[str,int]]: a list of tuples containing the names of the log files and the number of stationary points"""
102
+
103
+ results ={}
104
+
105
+ for output_file in glob(output_file_pattern):
106
+ results[output_file] = 0
107
+ with open(output_file) as current_file:
108
+ for line in current_file:
109
+ results[output_file] += "Stationary point" in line
110
+
111
+ results = [(key,results[key]) for key in results.keys()]
112
+ if len(results.keys()) == 0:
113
+ raise FileExistsError(f"No files found to match pattern '{output_file_pattern}'.")
114
+ results = sorted(results,key = lambda x: x[1])
115
+ return results
116
+
117
+
118
+ def get_n_geometry(log_file_name:str,n_geometry:int = -1):
119
+ """Grabs the nth geometry (zero indexed) from a log file. Defaults to the last geometry
120
+ Parameters
121
+ ----------
122
+ log_file_name (str): the path to a log file
123
+
124
+ n_geometry (int): the index of the desired geometry
125
+
126
+ Returns
127
+ -------
128
+ xyz_molecule"""
129
+
130
+ outfile = cclib.io.ccread(log_file_name)
131
+ atom_nums = outfile.atomnos
132
+ correct_geom = outfile.geometries[n_geometry]
133
+ return xyz_molecule.from_cclib_vals(atom_nums,correct_geom)
134
+
135
+ def rerun_from_last_geom(file_to_rerun):
136
+ """reruns a job from the last geometry found in the log file.
137
+ Parameters
138
+ ----------
139
+ file_to_rerun (str): the path to a log file
140
+
141
+ Returns
142
+ -------
143
+ None: reformats and resubmits g16 input"""
144
+ new_geometry = get_n_geometry(file_to_rerun)
145
+ old_input = g16_input.from_file(file_to_rerun[:-3]+"gjf")
146
+ old_input.geometry = new_geometry
147
+ old_input.write_file()
148
+ print(f"Rerunning {file_to_rerun[:-4]}")
149
+ os.system(f"sbatch {file_to_rerun[:-3]+"s"}")
150
+
151
+ def quick_rerun_optimizations(log_file_pattern:str="*.log"):
152
+ """Reruns any files with only one stationary point, and notifies about files with 0 stationary points
153
+ Parameters
154
+ ----------
155
+
156
+ log_file_pattern (str): a pattern for the path of files to rerun.
157
+
158
+ Returns
159
+ -------
160
+ None: reruns files with only 1 stationary point and prints advice for files with 0."""
161
+ statp_eval = stationary_point_count(log_file_pattern)
162
+
163
+ for file,statp_count in statp_eval:
164
+ if statp_count == 1:
165
+ rerun_from_last_geom(file)
166
+ elif statp_count == 0:
167
+ print(f"{file} requires manual check, not Stationary point found")
168
+
169
+
170
+
171
+
172
+
173
+