Skip to content

Commit dee3019

Browse files
committed
Cleaned up the implmenentation and docstrings of the cube_file_methods functions using ChatGPT
1 parent 9acaa7b commit dee3019

File tree

1 file changed

+187
-153
lines changed

1 file changed

+187
-153
lines changed

src/libra_py/cube_file_methods.py

Lines changed: 187 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -33,211 +33,245 @@
3333
import util.libutil as comn
3434
import libra_py.packages.cp2k.methods as CP2K_methods
3535

36-
3736
def read_cube(filename: str):
3837
"""
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.
4569
"""
4670

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)
5175
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
5676

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)
5981
if len(lines[nstart].split()) < 6:
6082
nstart += 1
6183

62-
nlines = len(lines) # the total number of lines
63-
6484
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))
7688

89+
return np.array(isovalues)
90+
7791

7892
def grid_volume(filename: str):
7993
"""
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.
92121
"""
93122

94-
f = open(filename, 'r')
95-
lines = f.readlines()
96-
f.close()
123+
with open(filename, 'r') as f:
124+
lines = f.readlines()
97125

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))
106134

107135
return dv
108136

109137

110138
def read_volumetric_data(filename: str):
111139
"""
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).
136182
"""
137183

138-
f = open(filename, 'r')
139-
lines = f.readlines()
140-
f.close()
184+
with open(filename, 'r') as f:
185+
lines = f.readlines()
141186

142-
# The number of atoms in the 3rd line
187+
# Number of atoms
143188
natoms = int(lines[2].split()[0])
144189

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
147191
nx = int(lines[3].split()[0])
148192
ny = int(lines[4].split()[0])
149193
nz = int(lines[5].split()[0])
150194

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)
163201
spacing_vector = axis_1 + axis_2 + axis_3
164202

165-
# First we read all the isovalues into a 1D list 'isovals'.
203+
# Read volumetric data (flattened)
204+
data_start = natoms + 6
166205
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))
167211

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
194213
x_grid = np.zeros((nx, ny, nz))
195214
y_grid = np.zeros((nx, ny, nz))
196215
z_grid = np.zeros((nx, ny, nz))
197216

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]
205224

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)])
213227

214228
return coordinates, x_grid, y_grid, z_grid, wave_fun, spacing_vector
215-
229+
216230

217231
def integrate_cube(cube_A, cube_B, grid_volume):
218232
"""
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.
228263
"""
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)
233264

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
237270

238271
return integral
239272

240273

274+
241275
def plot_cubes(params):
242276
"""
243277
This function plots the cubes for selected energy levels using VMD.

0 commit comments

Comments
 (0)