Coverage for /builds/kinetik161/ase/ase/io/gpumd.py: 90.68%

118 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1import numpy as np 

2 

3from ase import Atoms 

4from ase.data import atomic_masses, chemical_symbols 

5from ase.neighborlist import NeighborList 

6 

7 

8def find_nearest_index(array, value): 

9 array = np.asarray(array) 

10 idx = (np.abs(array - value)).argmin() 

11 return idx 

12 

13 

14def find_nearest_value(array, value): 

15 array = np.asarray(array) 

16 idx = (np.abs(array - value)).argmin() 

17 return array[idx] 

18 

19 

20def write_gpumd(fd, atoms, maximum_neighbors=None, cutoff=None, 

21 groupings=None, use_triclinic=False, species=None): 

22 """ 

23 Writes atoms into GPUMD input format. 

24 

25 Parameters 

26 ---------- 

27 fd : file 

28 File like object to which the atoms object should be written 

29 atoms : Atoms 

30 Input structure 

31 maximum_neighbors: int 

32 Maximum number of neighbors any atom can ever have (not relevant when 

33 using force constant potentials) 

34 cutoff: float 

35 Initial cutoff distance used for building the neighbor list (not 

36 relevant when using force constant potentials) 

37 groupings : list[list[list[int]]] 

38 Groups into which the individual atoms should be divided in the form of 

39 a list of list of lists. Specifically, the outer list corresponds to 

40 the grouping methods, of which there can be three at the most, which 

41 contains a list of groups in the form of lists of site indices. The 

42 sum of the lengths of the latter must be the same as the total number 

43 of atoms. 

44 use_triclinic: bool 

45 Use format for triclinic cells 

46 species : List[str] 

47 GPUMD uses integers to define atom types. This list allows customized 

48 such definitions (e.g, ['Pd', 'H'] means Pd is type 0 and H type 1). 

49 If None, this list is built by assigning each distinct species to 

50 an integer in the order of appearance in `atoms`. 

51 

52 Raises 

53 ------ 

54 ValueError 

55 Raised if parameters are incompatible 

56 """ 

57 

58 # Check velocties parameter 

59 if atoms.get_velocities() is None: 

60 has_velocity = 0 

61 else: 

62 has_velocity = 1 

63 velocities = atoms.get_velocities() 

64 

65 # Check groupings parameter 

66 if groupings is None: 

67 number_of_grouping_methods = 0 

68 else: 

69 number_of_grouping_methods = len(groupings) 

70 if number_of_grouping_methods > 3: 

71 raise ValueError('There can be no more than 3 grouping methods!') 

72 for g, grouping in enumerate(groupings): 

73 all_indices = [i for group in grouping for i in group] 

74 if len(all_indices) != len(atoms) or\ 

75 set(all_indices) != set(range(len(atoms))): 

76 raise ValueError('The indices listed in grouping method {} are' 

77 ' not compatible with the input' 

78 ' structure!'.format(g)) 

79 

80 # If not specified, estimate the maximum_neighbors 

81 if maximum_neighbors is None: 

82 if cutoff is None: 

83 cutoff = 0.1 

84 maximum_neighbors = 1 

85 else: 

86 nl = NeighborList([cutoff / 2] * len(atoms), skin=2, bothways=True) 

87 nl.update(atoms) 

88 maximum_neighbors = 0 

89 for atom in atoms: 

90 maximum_neighbors = max(maximum_neighbors, 

91 len(nl.get_neighbors(atom.index)[0])) 

92 maximum_neighbors *= 2 

93 maximum_neighbors = min(maximum_neighbors, 1024) 

94 

95 # Add header and cell parameters 

96 lines = [] 

97 if atoms.cell.orthorhombic and not use_triclinic: 

98 triclinic = 0 

99 else: 

100 triclinic = 1 

101 lines.append('{} {} {} {} {} {}'.format(len(atoms), maximum_neighbors, 

102 cutoff, triclinic, has_velocity, 

103 number_of_grouping_methods)) 

104 if triclinic: 

105 lines.append((' {}' * 12)[1:].format(*atoms.pbc.astype(int), 

106 *atoms.cell[:].flatten())) 

107 else: 

108 lines.append((' {}' * 6)[1:].format(*atoms.pbc.astype(int), 

109 *atoms.cell.lengths())) 

110 

111 # Create symbols-to-type map, i.e. integers starting at 0 

112 if not species: 

113 symbol_type_map = {} 

114 for symbol in atoms.get_chemical_symbols(): 

115 if symbol not in symbol_type_map: 

116 symbol_type_map[symbol] = len(symbol_type_map) 

117 else: 

118 if any(sym not in species 

119 for sym in set(atoms.get_chemical_symbols())): 

120 raise ValueError('The species list does not contain all chemical ' 

121 'species that are present in the atoms object.') 

122 else: 

123 symbol_type_map = {symbol: i for i, symbol in enumerate(species)} 

124 

125 # Add lines for all atoms 

126 for a, atm in enumerate(atoms): 

127 t = symbol_type_map[atm.symbol] 

128 line = (' {}' * 5)[1:].format(t, *atm.position, atm.mass) 

129 if has_velocity: 

130 line += (' {}' * 3).format(*velocities[a]) 

131 if groupings is not None: 

132 for grouping in groupings: 

133 for i, group in enumerate(grouping): 

134 if a in group: 

135 line += f' {i}' 

136 break 

137 lines.append(line) 

138 

139 # Write file 

140 fd.write('\n'.join(lines)) 

141 

142 

143def load_xyz_input_gpumd(fd, species=None, isotope_masses=None): 

144 """ 

145 Read the structure input file for GPUMD and return an ase Atoms object 

146 togehter with a dictionary with parameters and a types-to-symbols map 

147 

148 Parameters 

149 ---------- 

150 fd : file | str 

151 File object or name of file from which to read the Atoms object 

152 species : List[str] 

153 List with the chemical symbols that correspond to each type, will take 

154 precedence over isotope_masses 

155 isotope_masses: Dict[str, List[float]] 

156 Dictionary with chemical symbols and lists of the associated atomic 

157 masses, which is used to identify the chemical symbols that correspond 

158 to the types not found in species_types. The default is to find the 

159 closest match :data:`ase.data.atomic_masses`. 

160 

161 Returns 

162 ------- 

163 atoms : Atoms 

164 Atoms object 

165 input_parameters : Dict[str, int] 

166 Dictionary with parameters from the first row of the input file, namely 

167 'N', 'M', 'cutoff', 'triclinic', 'has_velocity' and 'num_of_groups' 

168 species : List[str] 

169 List with the chemical symbols that correspond to each type 

170 

171 Raises 

172 ------ 

173 ValueError 

174 Raised if the list of species is incompatible with the input file 

175 """ 

176 # Parse first line 

177 first_line = next(fd) 

178 input_parameters = {} 

179 keys = ['N', 'M', 'cutoff', 'triclinic', 'has_velocity', 

180 'num_of_groups'] 

181 types = [float if key == 'cutoff' else int for key in keys] 

182 for k, (key, typ) in enumerate(zip(keys, types)): 

183 input_parameters[key] = typ(first_line.split()[k]) 

184 

185 # Parse second line 

186 second_line = next(fd) 

187 second_arr = np.array(second_line.split()) 

188 pbc = second_arr[:3].astype(bool) 

189 if input_parameters['triclinic']: 

190 cell = second_arr[3:].astype(float).reshape((3, 3)) 

191 else: 

192 cell = np.diag(second_arr[3:].astype(float)) 

193 

194 # Parse all remaining rows 

195 n_rows = input_parameters['N'] 

196 n_columns = 5 + input_parameters['has_velocity'] * 3 +\ 

197 input_parameters['num_of_groups'] 

198 rest_lines = [next(fd) for _ in range(n_rows)] 

199 rest_arr = np.array([line.split() for line in rest_lines]) 

200 assert rest_arr.shape == (n_rows, n_columns) 

201 

202 # Extract atom types, positions and masses 

203 atom_types = rest_arr[:, 0].astype(int) 

204 positions = rest_arr[:, 1:4].astype(float) 

205 masses = rest_arr[:, 4].astype(float) 

206 

207 # Determine the atomic species 

208 if species is None: 

209 type_symbol_map = {} 

210 if isotope_masses is not None: 

211 mass_symbols = {mass: symbol for symbol, masses in 

212 isotope_masses.items() for mass in masses} 

213 symbols = [] 

214 for atom_type, mass in zip(atom_types, masses): 

215 if species is None: 

216 if atom_type not in type_symbol_map: 

217 if isotope_masses is not None: 

218 nearest_value = find_nearest_value( 

219 list(mass_symbols.keys()), mass) 

220 symbol = mass_symbols[nearest_value] 

221 else: 

222 symbol = chemical_symbols[ 

223 find_nearest_index(atomic_masses, mass)] 

224 type_symbol_map[atom_type] = symbol 

225 else: 

226 symbol = type_symbol_map[atom_type] 

227 else: 

228 if atom_type > len(species): 

229 raise Exception('There is no entry for atom type {} in the ' 

230 'species list!'.format(atom_type)) 

231 symbol = species[atom_type] 

232 symbols.append(symbol) 

233 

234 if species is None: 

235 species = [type_symbol_map[i] for i in sorted(type_symbol_map.keys())] 

236 

237 # Create the Atoms object 

238 atoms = Atoms(symbols=symbols, positions=positions, masses=masses, pbc=pbc, 

239 cell=cell) 

240 if input_parameters['has_velocity']: 

241 velocities = rest_arr[:, 5:8].astype(float) 

242 atoms.set_velocities(velocities) 

243 if input_parameters['num_of_groups']: 

244 start_col = 5 + 3 * input_parameters['has_velocity'] 

245 groups = rest_arr[:, start_col:].astype(int) 

246 atoms.info = {i: {'groups': groups[i, :]} for i in range(n_rows)} 

247 

248 return atoms, input_parameters, species 

249 

250 

251def read_gpumd(fd, species=None, isotope_masses=None): 

252 """ 

253 Read Atoms object from a GPUMD structure input file 

254 

255 Parameters 

256 ---------- 

257 fd : file | str 

258 File object or name of file from which to read the Atoms object 

259 species : List[str] 

260 List with the chemical symbols that correspond to each type, will take 

261 precedence over isotope_masses 

262 isotope_masses: Dict[str, List[float]] 

263 Dictionary with chemical symbols and lists of the associated atomic 

264 masses, which is used to identify the chemical symbols that correspond 

265 to the types not found in species_types. The default is to find the 

266 closest match :data:`ase.data.atomic_masses`. 

267 

268 Returns 

269 ------- 

270 atoms : Atoms 

271 Atoms object 

272 

273 Raises 

274 ------ 

275 ValueError 

276 Raised if the list of species is incompatible with the input file 

277 """ 

278 

279 return load_xyz_input_gpumd(fd, species, isotope_masses)[0]