Coverage for /builds/ase/ase/ase/io/cube.py: 89.32%

103 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3""" 

4IO support for the Gaussian cube format. 

5 

6See the format specifications on: 

7http://local.wasp.uwa.edu.au/~pbourke/dataformats/cube/ 

8""" 

9 

10import time 

11 

12import numpy as np 

13 

14from ase.atoms import Atoms 

15from ase.io import read 

16from ase.units import Bohr 

17 

18ATOMS = 'atoms' 

19CASTEP = 'castep' 

20DATA = 'data' 

21 

22 

23def write_cube(file_obj, atoms, data=None, origin=None, comment=None): 

24 """Function to write a cube file. 

25 

26 file_obj: str or file object 

27 File to which output is written. 

28 atoms: Atoms 

29 The Atoms object specifying the atomic configuration. 

30 data : 3-dim numpy array, optional (default = None) 

31 Array containing volumetric data as e.g. electronic density 

32 origin : 3-tuple 

33 Origin of the volumetric data (units: Angstrom) 

34 comment : str, optional (default = None) 

35 Comment for the first line of the cube file. 

36 """ 

37 

38 if data is None: 

39 data = np.ones((2, 2, 2)) 

40 data = np.asarray(data) 

41 

42 if data.dtype == complex: 

43 data = np.abs(data) 

44 

45 if comment is None: 

46 comment = "Cube file from ASE, written on " + time.strftime("%c") 

47 else: 

48 comment = comment.strip() 

49 file_obj.write(comment) 

50 

51 file_obj.write("\nOUTER LOOP: X, MIDDLE LOOP: Y, INNER LOOP: Z\n") 

52 

53 if origin is None: 

54 origin = np.zeros(3) 

55 else: 

56 origin = np.asarray(origin) / Bohr 

57 

58 file_obj.write( 

59 "{:5}{:12.6f}{:12.6f}{:12.6f}\n".format( 

60 len(atoms), *origin)) 

61 

62 for i in range(3): 

63 n = data.shape[i] 

64 d = atoms.cell[i] / n / Bohr 

65 file_obj.write("{:5}{:12.6f}{:12.6f}{:12.6f}\n".format(n, *d)) 

66 

67 positions = atoms.positions / Bohr 

68 numbers = atoms.numbers 

69 for Z, (x, y, z) in zip(numbers, positions): 

70 file_obj.write( 

71 "{:5}{:12.6f}{:12.6f}{:12.6f}{:12.6f}\n".format( 

72 Z, 0.0, x, y, z) 

73 ) 

74 

75 data.tofile(file_obj, sep="\n", format="%e") 

76 

77 

78def read_cube(file_obj, read_data=True, program=None, verbose=False): 

79 """Read atoms and data from CUBE file. 

80 

81 file_obj : str or file 

82 Location to the cube file. 

83 read_data : boolean 

84 If set true, the actual cube file content, i.e. an array 

85 containing the electronic density (or something else )on a grid 

86 and the dimensions of the corresponding voxels are read. 

87 program: str 

88 Use program='castep' to follow the PBC convention that first and 

89 last voxel along a direction are mirror images, thus the last 

90 voxel is to be removed. If program=None, the routine will try 

91 to catch castep files from the comment lines. 

92 verbose : bool 

93 Print some more information to stdout. 

94 

95 Returns a dict with the following keys: 

96 

97 * 'atoms': Atoms object 

98 * 'data' : (Nx, Ny, Nz) ndarray 

99 * 'origin': (3,) ndarray, specifying the cube_data origin. 

100 * 'spacing': (3, 3) ndarray, representing voxel size 

101 """ 

102 

103 readline = file_obj.readline 

104 line = readline() # the first comment line 

105 line = readline() # the second comment line 

106 

107 # The second comment line *CAN* contain information on the axes 

108 # But this is by far not the case for all programs 

109 axes = [] 

110 if "OUTER LOOP" in line.upper(): 

111 axes = ["XYZ".index(s[0]) for s in line.upper().split()[2::3]] 

112 if not axes: 

113 axes = [0, 1, 2] 

114 

115 # castep2cube files have a specific comment in the second line ... 

116 if "castep2cube" in line: 

117 program = CASTEP 

118 if verbose: 

119 print("read_cube identified program: castep") 

120 

121 # Third line contains actual system information: 

122 line = readline().split() 

123 num_atoms = int(line[0]) 

124 

125 # num_atoms can be negative. 

126 # Negative num_atoms indicates we have extra data to parse after 

127 # the coordinate information. 

128 has_labels = num_atoms < 0 

129 num_atoms = abs(num_atoms) 

130 

131 # There is an optional last field on this line which indicates 

132 # the number of values at each point. It is typically 1 (the default) 

133 # in which case it can be omitted, but it may also be > 1, 

134 # for example if there are multiple orbitals stored in the same cube. 

135 num_val = int(line[4]) if len(line) == 5 else 1 

136 

137 # Origin around which the volumetric data is centered 

138 # (at least in FHI aims): 

139 origin = np.array([float(x) * Bohr for x in line[1:4:]]) 

140 

141 cell = np.empty((3, 3)) 

142 shape = [] 

143 spacing = np.empty((3, 3)) 

144 

145 # The upcoming three lines contain the cell information 

146 for i in range(3): 

147 n, x, y, z = (float(s) for s in readline().split()) 

148 shape.append(int(n)) 

149 

150 # different PBC treatment in castep, basically the last voxel row is 

151 # identical to the first one 

152 if program == CASTEP: 

153 n -= 1 

154 cell[i] = n * Bohr * np.array([x, y, z]) 

155 spacing[i] = np.array([x, y, z]) * Bohr 

156 pbc = [(v != 0).any() for v in cell] 

157 

158 numbers = np.empty(num_atoms, int) 

159 positions = np.empty((num_atoms, 3)) 

160 for i in range(num_atoms): 

161 line = readline().split() 

162 numbers[i] = int(line[0]) 

163 positions[i] = [float(s) for s in line[2:]] 

164 

165 positions *= Bohr 

166 

167 atoms = Atoms(numbers=numbers, positions=positions, cell=cell, pbc=pbc) 

168 

169 # CASTEP will always have PBC, although the cube format does not 

170 # contain this kind of information 

171 if program == CASTEP: 

172 atoms.pbc = True 

173 

174 dct = {ATOMS: atoms} 

175 labels = [] 

176 

177 # If we originally had a negative num_atoms, parse the extra fields now. 

178 # The first field of the first line tells us how many other fields there 

179 # are to parse, but we have to guess how many rows this information is 

180 # split over. 

181 if has_labels: 

182 # Can't think of a more elegant way of doing this... 

183 fields = readline().split() 

184 nfields = int(fields[0]) 

185 labels.extend(fields[1:]) 

186 

187 while len(labels) < nfields: 

188 fields = readline().split() 

189 labels.extend(fields) 

190 

191 labels = [int(x) for x in labels] 

192 

193 if read_data: 

194 # Cube files can contain more than one density, 

195 # so we need to be a little bit careful about where one ends 

196 # and the next begins. 

197 raw_volume = [float(s) for s in file_obj.read().split()] 

198 # Split each value at each point into a separate list. 

199 raw_volumes = [np.array(raw_volume[offset::num_val]) 

200 for offset in range(num_val)] 

201 

202 datas = [] 

203 

204 # Adjust each volume in turn. 

205 for data in raw_volumes: 

206 data = data.reshape(shape) 

207 if axes != [0, 1, 2]: 

208 data = data.transpose(axes).copy() 

209 

210 if program == CASTEP: 

211 # Due to the PBC applied in castep2cube, the last entry 

212 # along each dimension equals the very first one. 

213 data = data[:-1, :-1, :-1] 

214 

215 datas.append(data) 

216 

217 datas = np.array(datas) 

218 

219 dct[DATA] = datas[0] 

220 dct["origin"] = origin 

221 dct["spacing"] = spacing 

222 dct["labels"] = labels 

223 dct["datas"] = datas 

224 

225 return dct 

226 

227 

228def read_cube_data(filename): 

229 """Wrapper function to read not only the atoms information from a cube file 

230 but also the contained volumetric data. 

231 """ 

232 dct = read(filename, format="cube", read_data=True, full_output=True) 

233 return dct[DATA], dct[ATOMS]