|
33 | 33 | import util.libutil as comn |
34 | 34 | import libra_py.packages.cp2k.methods as CP2K_methods |
35 | 35 |
|
36 | | - |
37 | 36 | def read_cube(filename: str): |
38 | 37 | """ |
39 | | - This function reads the wavefunction from a cube file and stores it in |
40 | | - a 1D numpy array |
41 | | - Args: |
42 | | - filename (string): the name of the .cube file to read |
43 | | - Returns: |
44 | | - isovalues (numpy.array) : the 1D array of the wavefunctions for all the points on the grid |
| 38 | + Read scalar field data (e.g., wavefunction or electron density) from a Gaussian |
| 39 | + .cube file and return it as a flattened 1D NumPy array. |
| 40 | +
|
| 41 | + This function parses the cube file header to determine where the volumetric |
| 42 | + data begins, then reads all grid values and stores them in a single array. |
| 43 | + The ordering of values follows the convention used in the cube file |
| 44 | + (typically x fastest, then y, then z). |
| 45 | +
|
| 46 | + Parameters |
| 47 | + ---------- |
| 48 | + filename : str |
| 49 | + Path to the .cube file. |
| 50 | +
|
| 51 | + Returns |
| 52 | + ------- |
| 53 | + isovalues : numpy.ndarray |
| 54 | + 1D array containing the scalar field values on the grid. |
| 55 | +
|
| 56 | + Notes |
| 57 | + ----- |
| 58 | + - The number of atoms is read from the third line of the file. Its absolute |
| 59 | + value is used to handle Gaussian cube files where this number may be negative. |
| 60 | + - The function skips: |
| 61 | + * 2 comment lines |
| 62 | + * 1 line with number of atoms and origin |
| 63 | + * 3 lines defining the grid |
| 64 | + * `natoms` lines with atomic coordinates |
| 65 | + - Some cube file variants include an extra line before the data block; this |
| 66 | + is handled automatically. |
| 67 | + - The output is flattened; reshaping into a 3D grid must be done separately |
| 68 | + using grid dimensions from the header if needed. |
45 | 69 | """ |
46 | 70 |
|
47 | | - f = open(filename, 'r') |
48 | | - lines = f.readlines() |
49 | | - f.close() |
50 | | - # The absolute value is for Gaussian since it might return a negative number for number of atoms |
| 71 | + with open(filename, 'r') as f: |
| 72 | + lines = f.readlines() |
| 73 | + |
| 74 | + # Number of atoms (may be negative in some Gaussian cube files) |
51 | 75 | natoms = abs(int(lines[2].split()[0])) |
52 | | - # We skip a few lines in the cube files, that go as follows: |
53 | | - # 2 lines - comments |
54 | | - # 1 line - the number of atoms, etc. |
55 | | - # 3 lines - the grid spacing and number of grid points in each dimensions |
56 | 76 |
|
57 | | - nstart = natoms + 2 + 1 + 3 # the index of the first line containing wfc data |
58 | | - # For Gaussian cube files |
| 77 | + # Index of the first line containing volumetric data |
| 78 | + nstart = natoms + 2 + 1 + 3 |
| 79 | + |
| 80 | + # Handle cube files with an extra line before data (format variation) |
59 | 81 | if len(lines[nstart].split()) < 6: |
60 | 82 | nstart += 1 |
61 | 83 |
|
62 | | - nlines = len(lines) # the total number of lines |
63 | | - |
64 | 84 | isovalues = [] |
65 | | - for i in range(nstart, nlines): |
66 | | - tmp = lines[i].split() |
67 | | - ncols = len(tmp) |
68 | | - |
69 | | - for j in range(ncols): |
70 | | - isovalues.append(float(tmp[j])) |
71 | | - |
72 | | - isovalues = np.array(isovalues) |
73 | | - # data = np.loadtxt(filename,skiprows=n) |
74 | | - |
75 | | - return isovalues |
| 85 | + for line in lines[nstart:]: |
| 86 | + for val in line.split(): |
| 87 | + isovalues.append(float(val)) |
76 | 88 |
|
| 89 | + return np.array(isovalues) |
| 90 | + |
77 | 91 |
|
78 | 92 | def grid_volume(filename: str): |
79 | 93 | """ |
80 | | - This function reads the wavefunction from a cube file and calculate |
81 | | - the grid volum using the X-, Y- and Z-axis of the volumetric region |
82 | | - which are placed in the 4th, 5th and 6th line of the cube file structure |
83 | | -
|
84 | | - Args: |
85 | | -
|
86 | | - filename (string): The name of the .cube file to read. |
87 | | -
|
88 | | - Returns: |
89 | | -
|
90 | | - dv (float): The grid volume in Bohr^3. |
91 | | -
|
| 94 | + Compute the volume element (voxel volume) of a grid cell from a Gaussian |
| 95 | + .cube file. |
| 96 | +
|
| 97 | + The cube file defines the volumetric grid using three lattice vectors |
| 98 | + (one per axis), given in lines 4–6 of the file. Each vector corresponds |
| 99 | + to the spacing and direction of the grid along x, y, and z. The volume |
| 100 | + of a single grid cell is the absolute value of the determinant of these |
| 101 | + three vectors. |
| 102 | +
|
| 103 | + Parameters |
| 104 | + ---------- |
| 105 | + filename : str |
| 106 | + Path to the .cube file. |
| 107 | +
|
| 108 | + Returns |
| 109 | + ------- |
| 110 | + dv : float |
| 111 | + Volume of a single grid cell (voxel) in Bohr³. |
| 112 | +
|
| 113 | + Notes |
| 114 | + ----- |
| 115 | + - Lines 4–6 of the cube file contain: |
| 116 | + * number of grid points along each axis (first column) |
| 117 | + * corresponding lattice vector components (remaining columns) |
| 118 | + - Only the vector components are used here. |
| 119 | + - The total grid volume would be `dv * Nx * Ny * Nz`, where Nx, Ny, Nz |
| 120 | + are the number of grid points along each axis. |
92 | 121 | """ |
93 | 122 |
|
94 | | - f = open(filename, 'r') |
95 | | - lines = f.readlines() |
96 | | - f.close() |
| 123 | + with open(filename, 'r') as f: |
| 124 | + lines = f.readlines() |
97 | 125 |
|
98 | | - # We use the 3rd, 4th and 5th row in the lines to obtain |
99 | | - # the axes of the parallelpiped into a numpy array (Voxel). |
100 | | - axis_1 = [float(lines[3].split()[1]), float(lines[3].split()[2]), float(lines[3].split()[3])] |
101 | | - axis_2 = [float(lines[4].split()[1]), float(lines[4].split()[2]), float(lines[4].split()[3])] |
102 | | - axis_3 = [float(lines[5].split()[1]), float(lines[5].split()[2]), float(lines[5].split()[3])] |
103 | | - vol_element = np.array([axis_1, axis_2, axis_3]) |
104 | | - # Then we calculate the determinant of Voxel to obtain the volume. |
105 | | - dv = np.absolute(np.linalg.det(vol_element)) |
| 126 | + # Extract lattice vectors (skip the first column: number of grid points) |
| 127 | + axis_1 = [float(x) for x in lines[3].split()[1:4]] |
| 128 | + axis_2 = [float(x) for x in lines[4].split()[1:4]] |
| 129 | + axis_3 = [float(x) for x in lines[5].split()[1:4]] |
| 130 | + |
| 131 | + # Form the voxel matrix and compute its volume |
| 132 | + voxel = np.array([axis_1, axis_2, axis_3]) |
| 133 | + dv = abs(np.linalg.det(voxel)) |
106 | 134 |
|
107 | 135 | return dv |
108 | 136 |
|
109 | 137 |
|
110 | 138 | def read_volumetric_data(filename: str): |
111 | 139 | """ |
112 | | - This function reads the volumetric data in a format used for plotting |
113 | | - of the data. The difference between 'read_cube' function is that it will |
114 | | - show the data in a 3D array which has the shape as the number of grid points |
115 | | - for each of the X-, Y-, and Z-axis in the 4th to 6th line of the cube file. |
116 | | - This function will return the grid points in each axis, the coordinates of the |
117 | | - structure and the spacing vector which is used to plot the isosurfaces of the |
118 | | - molecular orbitals. |
119 | | -
|
120 | | - Args: |
121 | | -
|
122 | | - filename (string): The name of the .cube file. |
123 | | -
|
124 | | - Returns: |
125 | | -
|
126 | | - coordinates (2D numpy array): The coordinates of the molecule structure in |
127 | | - the same format shown in the .cube file. |
128 | | -
|
129 | | - x_grid, y_grid, z_grid (3D numpy array): Containing the grid points for each of the X-, |
130 | | - Y-, and Z- axis. |
131 | | -
|
132 | | - wave_fun (3D numpy array): The volumetric data in a 3D numpy array format. |
133 | | -
|
134 | | - spacing_vector (numpy 1D array): The spacing vector used for plotting the isosurfaces. |
135 | | -
|
| 140 | + Read volumetric data from a Gaussian .cube file and return it in a |
| 141 | + structured 3D form suitable for visualization and analysis. |
| 142 | +
|
| 143 | + This function parses the cube file header to extract grid dimensions, |
| 144 | + lattice vectors, and atomic coordinates, then reshapes the volumetric |
| 145 | + data into a 3D array. It also constructs real-space grid coordinates |
| 146 | + corresponding to each voxel. |
| 147 | +
|
| 148 | + Parameters |
| 149 | + ---------- |
| 150 | + filename : str |
| 151 | + Path to the .cube file. |
| 152 | +
|
| 153 | + Returns |
| 154 | + ------- |
| 155 | + coordinates : numpy.ndarray |
| 156 | + Array of shape (natoms, 5) containing atomic data as read from the |
| 157 | + cube file (atomic number, charge, x, y, z). Values are returned as strings. |
| 158 | +
|
| 159 | + x_grid, y_grid, z_grid : numpy.ndarray |
| 160 | + 3D arrays of shape (nx, ny, nz) containing the Cartesian coordinates |
| 161 | + of each grid point. |
| 162 | +
|
| 163 | + wave_fun : numpy.ndarray |
| 164 | + 3D array of shape (nx, ny, nz) containing the volumetric data |
| 165 | + (e.g., wavefunction or electron density). |
| 166 | +
|
| 167 | + spacing_vector : numpy.ndarray |
| 168 | + Vector representing the effective grid spacing (sum of the three |
| 169 | + lattice vectors). Often used in visualization routines. |
| 170 | +
|
| 171 | + Notes |
| 172 | + ----- |
| 173 | + - The cube file structure is: |
| 174 | + * 2 comment lines |
| 175 | + * 1 line: number of atoms and origin |
| 176 | + * 3 lines: grid size and lattice vectors |
| 177 | + * natoms lines: atomic coordinates |
| 178 | + * remaining lines: volumetric data |
| 179 | + - Grid vectors are given in Bohr; no unit conversion is applied. |
| 180 | + - The volumetric data is assumed to be ordered with x varying fastest, |
| 181 | + followed by y, then z (standard cube convention). |
136 | 182 | """ |
137 | 183 |
|
138 | | - f = open(filename, 'r') |
139 | | - lines = f.readlines() |
140 | | - f.close() |
| 184 | + with open(filename, 'r') as f: |
| 185 | + lines = f.readlines() |
141 | 186 |
|
142 | | - # The number of atoms in the 3rd line |
| 187 | + # Number of atoms |
143 | 188 | natoms = int(lines[2].split()[0]) |
144 | 189 |
|
145 | | - # The number of voxels defined for each axis obtained from |
146 | | - # the first elements of the 4th, 5th, and 6th line of the cube files |
| 190 | + # Grid dimensions |
147 | 191 | nx = int(lines[3].split()[0]) |
148 | 192 | ny = int(lines[4].split()[0]) |
149 | 193 | nz = int(lines[5].split()[0]) |
150 | 194 |
|
151 | | - # The three vectors below are the same vectors which are present in lines 4th to 6th |
152 | | - # which are used to create the 3D numpy array of the grid points (x, y, z) used to plot |
153 | | - # the isosurfaces. |
154 | | - # Here we use the same unit as is used in the .cube file structure which is Bohr |
155 | | - # with no need for unit conversion. This makes the plotting easier. |
156 | | - axis_1 = np.array([float(lines[3].split()[1]), float(lines[3].split()[2]), float(lines[3].split()[3])]) |
157 | | - axis_2 = np.array([float(lines[4].split()[1]), float(lines[4].split()[2]), float(lines[4].split()[3])]) |
158 | | - axis_3 = np.array([float(lines[5].split()[1]), float(lines[5].split()[2]), float(lines[5].split()[3])]) |
159 | | - |
160 | | - # The spacing vector. This will be used in the 'marching_cubes_lewiner' |
161 | | - # function to define the vertices and faces for 'plot_trisurf' function. |
162 | | - # This vector is obtained from the sum of the three axis in the cube file as above. |
| 195 | + # Lattice vectors |
| 196 | + axis_1 = np.array([float(x) for x in lines[3].split()[1:4]]) |
| 197 | + axis_2 = np.array([float(x) for x in lines[4].split()[1:4]]) |
| 198 | + axis_3 = np.array([float(x) for x in lines[5].split()[1:4]]) |
| 199 | + |
| 200 | + # Effective spacing vector (useful for visualization libraries) |
163 | 201 | spacing_vector = axis_1 + axis_2 + axis_3 |
164 | 202 |
|
165 | | - # First we read all the isovalues into a 1D list 'isovals'. |
| 203 | + # Read volumetric data (flattened) |
| 204 | + data_start = natoms + 6 |
166 | 205 | isovals = [] |
| 206 | + for line in lines[data_start:]: |
| 207 | + isovals.extend(float(val) for val in line.split()) |
| 208 | + |
| 209 | + # Reshape into 3D array (cube convention) |
| 210 | + wave_fun = np.array(isovals).reshape((nx, ny, nz)) |
167 | 211 |
|
168 | | - # Starting from the line which the volumetric data starts which is the (natoms+3+2+1+1)th line. |
169 | | - for i in range(natoms + 3 + 2 + 1, len(lines)): |
170 | | - for j in range(0, len(lines[i].split())): |
171 | | - isovals.append(float(lines[i].split()[j])) |
172 | | - |
173 | | - # Define the volumetric numpy array |
174 | | - wave_fun = np.zeros((nx, ny, nz)) |
175 | | - |
176 | | - # Setting up the counters to append the isovalues in a 3D numpy array |
177 | | - c = 0 |
178 | | - c1 = 0 |
179 | | - c2 = 0 |
180 | | - |
181 | | - for i in range(0, len(isovals)): |
182 | | - if c2 != nx: |
183 | | - wave_fun[c2][c1][c] = isovals[i] |
184 | | - c = c + 1 |
185 | | - if c % nz == 0: |
186 | | - c = 0 |
187 | | - c1 = c1 + 1 |
188 | | - if c1 % ny == 0: |
189 | | - c1 = 0 |
190 | | - c2 = c2 + 1 |
191 | | - |
192 | | - # Now define the x, y, and z 3D arrays to store the grids |
193 | | - # which is then used for plotting the isosurfaces. |
| 212 | + # Construct coordinate grids |
194 | 213 | x_grid = np.zeros((nx, ny, nz)) |
195 | 214 | y_grid = np.zeros((nx, ny, nz)) |
196 | 215 | z_grid = np.zeros((nx, ny, nz)) |
197 | 216 |
|
198 | | - # Defining each element of the grid points. |
199 | | - for i in range(0, nx): |
200 | | - for j in range(0, ny): |
201 | | - for k in range(0, nz): |
202 | | - x_grid[i][j][k] = axis_1[0] * i + axis_2[0] * j + axis_3[0] * k |
203 | | - y_grid[i][j][k] = axis_1[1] * i + axis_2[1] * j + axis_3[1] * k |
204 | | - z_grid[i][j][k] = axis_1[2] * i + axis_2[2] * j + axis_3[2] * k |
| 217 | + for i in range(nx): |
| 218 | + for j in range(ny): |
| 219 | + for k in range(nz): |
| 220 | + r = i * axis_1 + j * axis_2 + k * axis_3 |
| 221 | + x_grid[i, j, k] = r[0] |
| 222 | + y_grid[i, j, k] = r[1] |
| 223 | + z_grid[i, j, k] = r[2] |
205 | 224 |
|
206 | | - # For plottin the atoms in the molecule we have to read the |
207 | | - # coordinates in xyz format which starts from the 7th line. |
208 | | - coordinates = [] |
209 | | - for i in range(6, natoms + 6): |
210 | | - coordinates.append(lines[i].split()) |
211 | | - |
212 | | - coordinates = np.array(coordinates) |
| 225 | + # Atomic coordinates (raw format from file) |
| 226 | + coordinates = np.array([lines[i].split() for i in range(6, natoms + 6)]) |
213 | 227 |
|
214 | 228 | return coordinates, x_grid, y_grid, z_grid, wave_fun, spacing_vector |
215 | | - |
| 229 | + |
216 | 230 |
|
217 | 231 | def integrate_cube(cube_A, cube_B, grid_volume): |
218 | 232 | """ |
219 | | - This function calculates the element-wise multiplication of two numpy arrays |
220 | | - and sums their product. Then, it will multiply the sum by 'dv' element to |
221 | | - compute the integral of two wavefunction represented as .cube files. |
222 | | - Args: |
223 | | - cube_A, cube_B (numpy array): The elements of the cube files in a 1D array |
224 | | - obtained from the 'read_cube' function. |
225 | | - grid_volume (float): The volume of the voxel obtained from grid_volume function. |
226 | | - Returns: |
227 | | - integral (float): The integration between two wavefunction in the .cube files. |
| 233 | + Compute the numerical integral of the product of two scalar fields |
| 234 | + defined on the same volumetric grid (e.g., wavefunctions from .cube files). |
| 235 | +
|
| 236 | + The integral is approximated as a discrete sum over all grid points: |
| 237 | + ∫ A(r) B(r) dV ≈ Σ_i A_i * B_i * dv |
| 238 | + where dv is the volume of a single grid cell (voxel). |
| 239 | +
|
| 240 | + Parameters |
| 241 | + ---------- |
| 242 | + cube_A, cube_B : numpy.ndarray |
| 243 | + 1D arrays containing the volumetric data (e.g., wavefunctions or |
| 244 | + densities) sampled on the same grid. These are typically obtained |
| 245 | + from a cube file reader (e.g., `read_cube`). |
| 246 | + Both arrays must have the same shape and ordering. |
| 247 | +
|
| 248 | + grid_volume : float |
| 249 | + Volume of a single grid cell (voxel), typically computed using |
| 250 | + `grid_volume`. Units are usually Bohr³. |
| 251 | +
|
| 252 | + Returns |
| 253 | + ------- |
| 254 | + integral : float |
| 255 | + Numerical approximation of the integral ∫ A(r) B(r) dV. |
| 256 | +
|
| 257 | + Notes |
| 258 | + ----- |
| 259 | + - This operation corresponds to an overlap integral if A and B are |
| 260 | + wavefunctions defined on the same grid. |
| 261 | + - No normalization or unit conversion is performed. |
| 262 | + - The accuracy depends on the grid resolution and spacing. |
228 | 263 | """ |
229 | | - # Compute the element-wise multiplication of the two cube files |
230 | | - # which were previously obtained in 1D numpy arrays and store |
231 | | - # them into another 1D numpy array. |
232 | | - product = np.multiply(cube_A, cube_B) |
233 | 264 |
|
234 | | - # Compute the summation of the above matrix |
235 | | - summation = product.sum() |
236 | | - integral = summation * grid_volume |
| 265 | + # Element-wise product |
| 266 | + product = cube_A * cube_B |
| 267 | + |
| 268 | + # Discrete integration |
| 269 | + integral = product.sum() * grid_volume |
237 | 270 |
|
238 | 271 | return integral |
239 | 272 |
|
240 | 273 |
|
| 274 | + |
241 | 275 | def plot_cubes(params): |
242 | 276 | """ |
243 | 277 | This function plots the cubes for selected energy levels using VMD. |
|
0 commit comments