Coverage for hiphive/force_constant_potential.py: 96%

167 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-11-28 11:20 +0000

1""" 

2This module introduces the ForceConstantPotential object which acts as the 

3finalized force constant model. 

4""" 

5 

6import pickle 

7import tarfile 

8import numpy as np 

9 

10from collections import Counter 

11from typing import Any, Dict, List 

12from .core.atoms import Atoms 

13from .force_constant_model import ForceConstantModel 

14from .core.orbits import Orbit 

15from .core.orbits import OrientationFamily 

16from .core.tensors import rotation_to_cart_coord, rotate_tensor 

17from .input_output.read_write_files import (add_items_to_tarfile_pickle, 

18 add_items_to_tarfile_custom, 

19 add_list_to_tarfile_custom, 

20 read_items_pickle, 

21 read_list_custom) 

22from .input_output.logging_tools import logger 

23 

24 

25logger = logger.getChild('ForceConstantPotential') 

26 

27 

28class ForceConstantPotential: 

29 """ A finalized force constant model. Can produce force constants for any 

30 structure compatible with the structure for which the model was set up. 

31 

32 Parameters 

33 ---------- 

34 cs : ClusterSpace 

35 The cluster space the model is based upon 

36 parameters : numpy.ndarray 

37 The fitted paramteres 

38 metadata : dict 

39 metadata dictionary, will be pickled when object is written to file 

40 """ 

41 

42 def __init__(self, cs, parameters, metadata=None): 

43 

44 self._prim = cs.primitive_structure.copy() 

45 self.cluster_list = cs.cluster_list.copy() 

46 self.atom_list = cs.atom_list.copy() 

47 self.orbits = [] 

48 self.spacegroup = cs.spacegroup 

49 self._config = cs._config 

50 self.cs_summary = cs.summary 

51 

52 # add metadata 

53 if metadata is None: 

54 metadata = dict() 

55 self._metadata = metadata 

56 self._add_default_metadata() 

57 

58 # Extract the eigentensors from the cluster space and use the paramters 

59 # to construct the finalized force constants 

60 parameters = cs._map_parameters(parameters) 

61 p = 0 

62 for orb in cs.orbits: 

63 new_orbit = Orbit() 

64 fc = np.zeros(orb.eigentensors[0].shape) 

65 for et, a in zip(orb.eigentensors, parameters[p:]): 

66 fc += et * a 

67 new_orbit.force_constant = fc 

68 new_orbit.order = orb.order 

69 new_orbit.radius = orb.radius 

70 new_orbit.maximum_distance = orb.maximum_distance 

71 for of in orb.orientation_families: 

72 new_of = OrientationFamily() 

73 new_of.cluster_indices = of.cluster_indices.copy() 

74 sym_ind = of.symmetry_index 

75 R = rotation_to_cart_coord(cs.rotation_matrices[sym_ind], 

76 self.primitive_structure.cell) 

77 fc = rotate_tensor(new_orbit.force_constant, R.T) 

78 perm = cs.permutations[of.permutation_indices[0]] 

79 new_of.force_constant = fc.transpose(perm) 

80 new_orbit.orientation_families.append(new_of) 

81 self.orbits.append(new_orbit) 

82 p += len(orb.eigentensors) 

83 

84 @property 

85 def symprec(self): 

86 return self._config['symprec'] 

87 

88 def write(self, filename): 

89 """ Writes a ForceConstantPotential to file. 

90 

91 Parameters 

92 ---------- 

93 filename : str 

94 name of file to write ForceConstantPotential to 

95 """ 

96 

97 # Create a tar archive 

98 if isinstance(filename, str): 

99 tar_file = tarfile.open(name=filename, mode='w') 

100 else: 

101 raise ValueError('filename must be str') 

102 

103 # objects with custom write 

104 add_list_to_tarfile_custom(tar_file, self.orbits, 'orbits') 

105 

106 # prim with its builtin write/read functions. Note prim is a hiphive.core.Atoms object 

107 items_custom = {'_prim': self._prim} 

108 add_items_to_tarfile_custom(tar_file, items_custom) 

109 

110 # Attributes in pickle format 

111 pickle_attributes = ['_config', '_metadata', 'spacegroup', 'atom_list', 

112 'cluster_list', 'cs_summary'] 

113 items_pickle = dict() 

114 for attribute in pickle_attributes: 

115 items_pickle[attribute] = self.__getattribute__(attribute) 

116 add_items_to_tarfile_pickle(tar_file, items_pickle, 'attributes') 

117 

118 # Done! 

119 tar_file.close() 

120 

121 @staticmethod 

122 def read(filename): 

123 """ Reads a ForceConstantPotentialfrom file. 

124 

125 Parameters 

126 ---------- 

127 filename : str 

128 name of input file to load ForceConstantPotential from 

129 

130 Returns 

131 ------- 

132 ForceConstantPotential 

133 the original object as stored in the file 

134 """ 

135 

136 # Try usage of old read format, Remove in hiphive 1.0 

137 try: 

138 old_fcp = ForceConstantPotential._read_old(filename) 

139 logger.warning('This fcp was written with a version <1.0. Please rewrite it.') 

140 return old_fcp 

141 except Exception: 

142 pass 

143 

144 # Instantiate empty cs obj. 

145 fcp = ForceConstantPotential.__new__(ForceConstantPotential) 

146 

147 # Load from file on disk 

148 if type(filename) is str: 

149 tar_file = tarfile.open(mode='r', name=filename) 

150 else: 

151 raise ValueError('filename must be str') 

152 

153 # Attributes with custom read 

154 fileobj = tar_file.extractfile('_prim') 

155 fcp._prim = Atoms.read(fileobj) 

156 

157 fcp.orbits = read_list_custom(tar_file, 'orbits', Orbit.read) 

158 

159 # Attributes 

160 attributes = read_items_pickle(tar_file, 'attributes') 

161 for name, value in attributes.items(): 

162 fcp.__setattr__(name, value) 

163 

164 # Done! 

165 tar_file.close() 

166 return fcp 

167 

168 # TODO: Remove? 

169 def _write_old(self, f): 

170 """Writes a force constant potential to file using old format. 

171 

172 Parameters 

173 ---------- 

174 f : str or file object 

175 name of input file (str) or stream to write to (file object) 

176 """ 

177 if isinstance(f, str): 177 ↛ 181line 177 didn't jump to line 181 because the condition on line 177 was always true

178 with open(f, 'wb') as fobj: 

179 pickle.dump(self, fobj) 

180 else: 

181 try: 

182 pickle.dump(self, f) 

183 except Exception: 

184 raise Exception('Failed writing to file.') 

185 

186 @staticmethod 

187 def _read_old(f): 

188 """Reads a force constant potential from file in old format. 

189 

190 Parameters 

191 ---------- 

192 f : str or file object 

193 name of input file (str) or stream to load from (file object) 

194 

195 Returns 

196 ------- 

197 ForceConstantPotential 

198 the original object as stored in the file 

199 """ 

200 if isinstance(f, str): 

201 with open(f, 'rb') as fobj: 

202 

203 # This allows for reading FCPs with ASE-3.17 and 3.18 

204 fcp = pickle.load(fobj) 

205 _prim = fcp._prim 

206 

207 if hasattr(_prim, '_cell'): # 3.17 207 ↛ 208line 207 didn't jump to line 208 because the condition on line 207 was never true

208 cell = _prim._cell 

209 else: # 3.18 

210 cell = _prim.cell[:] 

211 

212 # assume PBC True (as it has to be True in hiphive) 

213 pbc = [True, True, True] 

214 

215 new_prim = Atoms( 

216 symbols=_prim.symbols, positions=_prim.positions, cell=cell, pbc=pbc) 

217 fcp._prim = new_prim 

218 return fcp 

219 else: 

220 return pickle.load(f) 

221 

222 @property 

223 def metadata(self): 

224 """ dict : metadata associated with force constant potential """ 

225 return self._metadata 

226 

227 @property 

228 def primitive_structure(self): 

229 """ ase.Atoms : atomic structure """ 

230 return self._prim.copy() 

231 

232 @property 

233 def orbit_data(self) -> List[Dict[str, Any]]: 

234 """list of dictionaries containing detailed information for each 

235 orbit, e.g. cluster radius and force constant 

236 """ 

237 data = [] 

238 for orbit_index, orbit in enumerate(self.orbits): 

239 d = {} 

240 d['index'] = orbit_index 

241 d['order'] = orbit.order 

242 d['radius'] = orbit.radius 

243 d['maximum_distance'] = orbit.maximum_distance 

244 d['n_clusters'] = len(orbit.orientation_families) 

245 

246 types = [] 

247 for atom_ind in self.cluster_list[orbit.prototype_index]: 

248 types.append(self.primitive_structure.numbers[ 

249 self.atom_list[atom_ind].site]) 

250 d['prototype_cluster'] = self.cluster_list[orbit.prototype_index] 

251 d['prototype_atom_types'] = types 

252 

253 d['geometrical_order'] = len(set(d['prototype_cluster'])) 

254 d['force_constant'] = orbit.force_constant 

255 d['force_constant_norm'] = np.linalg.norm(orbit.force_constant) 

256 data.append(d) 

257 return data 

258 

259 def print_tables(self): 

260 """ Prints information concerning the underlying cluster space to stdout,including, 

261 e.g., the number of cluster, orbits, and parameters by order and number of bodies. """ 

262 self.cs_summary.print_tables() 

263 

264 def get_force_constants(self, atoms): 

265 """ Return the force constants of a compatible structure. 

266 

267 Parameters 

268 ---------- 

269 atoms : ase.Atoms 

270 input structure 

271 

272 Returns 

273 ------- 

274 ForceConstants 

275 force constants 

276 """ 

277 return ForceConstantModel(atoms, self).get_force_constants() 

278 

279 def __str__(self): 

280 orbits = self.orbit_data 

281 orbit_counts = Counter([orbit['order'] for orbit in orbits]) 

282 cluster_counts = Counter() 

283 for orbit in orbits: 

284 cluster_counts[orbit['order']] += orbit['n_clusters'] 

285 

286 n = 54 

287 s = [] 

288 s.append(' ForceConstantPotential '.center(n, '=')) 

289 s.append(f'Spacegroup {self.spacegroup}') 

290 s.append(f'Cell:\n{self.primitive_structure.cell}') 

291 s.append(f'Basis:\n{self.primitive_structure.basis}') 

292 s.append(f'Numbers: {self.primitive_structure.numbers}') 

293 

294 s.append(f'Cutoff matrix:\n{self.cs_summary._cutoff_matrix}') 

295 

296 for order in sorted(orbit_counts.keys()): 

297 n_dofs = self.cs_summary.ndofs_by_order[order] 

298 s.append(f'Order {order}, #orbits {orbit_counts[order]}, #cluster ' 

299 f'{cluster_counts[order]}, #parameters {n_dofs}') 

300 s.append(f'Total number of orbits: {len(orbits)}') 

301 s.append(f'total number of clusters: {sum(cluster_counts.values())}') 

302 s.append(f'total number of parameters: {sum(self.cs_summary.ndofs_by_order.values())}') 

303 s.append(''.center(n, '=')) 

304 return '\n'.join(s) 

305 

306 def __repr__(self): 

307 return 'ForceConstantPotential(ClusterSpace({!r}, ...), [...])'.format( 

308 self.primitive_structure) 

309 

310 def _add_default_metadata(self): 

311 """Adds default metadata to metadata dict.""" 

312 import getpass 

313 import socket 

314 from datetime import datetime 

315 from . import __version__ as hiphive_version 

316 

317 self._metadata['date_created'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') 

318 self._metadata['username'] = getpass.getuser() 

319 self._metadata['hostname'] = socket.gethostname() 

320 self._metadata['hiphive_version'] = hiphive_version