Coverage for mchammer/free_energy_tools.py: 88%

88 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-12-26 04:12 +0000

1from typing import List 

2from icet import ClusterSpace 

3from icet.core.sublattices import Sublattices 

4 

5import numpy as np 

6from scipy.integrate import cumulative_trapezoid 

7 

8from ase import Atoms 

9from ase.units import kB 

10 

11from mchammer import DataContainer 

12 

13import operator 

14from collections import Counter 

15from math import factorial 

16from functools import reduce 

17 

18 

19def _lambda_function_forward(n_steps: int, step: int) -> float: 

20 """ 

21 Returns the current lambda value for a backward calculation. 

22 

23 Parameters 

24 ---------- 

25 n_steps 

26 Total number of steps.. 

27 step 

28 Current step. 

29 """ 

30 x = (step + 1) / (n_steps) 

31 lam = x**5*(70*x**4 - 315*x**3 + 540*x**2 - 420*x + 126) 

32 # Due to numerical precision lambda may be slightly outside the interval [0.0, 1.0] 

33 lam = np.clip(lam, 0.0, 1.0) 

34 return lam 

35 

36 

37def _lambda_function_backward(n_steps: int, step: int) -> float: 

38 """ 

39 Returns the current lambda value for a backward calculation. 

40 

41 Parameters 

42 ---------- 

43 n_steps 

44 Total number of steps. 

45 step 

46 Current step. 

47 """ 

48 return 1 - _lambda_function_forward(n_steps, step) 

49 

50 

51def _stirling(n: int) -> float: 

52 """ Stirling approximation of log(n!). """ 

53 return n * np.log(n) - n + 0.5 * np.log(2*np.pi*n) 

54 

55 

56def _lognpermutations(array: List[int]) -> float: 

57 """If _npermutations becomes too large we resort to calculating the 

58 logarithm directly using Stirling's approximation, i.e., we 

59 calculate 

60 

61 .. math:: 

62 

63 log(n!) - (log(a1!) + log(a2!) + log(a3!) + ... + log(ak!) 

64 

65 where we denote in the code 

66 

67 .. math:: 

68 

69 v n = log(n!) 

70 den = (log(a1!) + log(a2!) + log(a3!) + ... + log(ak!) 

71 

72 """ 

73 

74 n = _stirling(len(array)) 

75 # Gets the number of each unique atom type 

76 mults = Counter(array).values() 

77 den = reduce(operator.add, 

78 (_stirling(v) for v in mults), 

79 1) 

80 return n - den 

81 

82 

83def _npermutations(array: List[int]) -> float: 

84 """ 

85 Returns the total number of ways we can permutate the array which is given by 

86 

87 .. math:: 

88 

89 n! / (a1! * a2! * a3! * ... * ak!) 

90 

91 where we denote in the code 

92 

93 .. math:: 

94 

95 den = (a1! * a2! * a3! * ... * ak!) 

96 """ 

97 n = factorial(len(array)) 

98 # Gets the number of each unique atom type 

99 a = Counter(array).values() 

100 den = reduce(operator.mul, 

101 (factorial(v) for v in a), 

102 1) 

103 return n / den 

104 

105 

106def _ideal_mixing_entropy(swap_sublattice_probabilities: List[int], 

107 atoms_on_sublattices: np.ndarray, 

108 boltzmann_constant: float) -> float: 

109 """ 

110 Calculates the ideal mixing entropy for the supercell. 

111 

112 Parameters 

113 ---------- 

114 swap_sublattice_probabilites 

115 Sublattices that are active during the MC simulation. 

116 atoms_on_sublattices 

117 Sorted atomic numbers in sublattices lists. 

118 boltzmann_constant 

119 Value of Boltzmann constant in the units used in the MC simulation. 

120 """ 

121 log_multiplicity = [] 

122 sublattices = np.array(swap_sublattice_probabilities) > 0 

123 for aos in atoms_on_sublattices[sublattices]: 

124 try: 

125 log_multiplicity.append(np.log(_npermutations(aos))) 

126 except Exception: 

127 log_multiplicity.append(_lognpermutations(aos)) 

128 return boltzmann_constant * np.sum(log_multiplicity) 

129 

130 

131def _get_atoms_on_sublattice(structure: Atoms, 

132 sublattices: Sublattices) -> np.ndarray: 

133 """Sorts the atom symbols in the structure to lists involving 

134 specific sublattices, for example, 

135 

136 [[5, 5, 3, 3, 3], [11, 11, 10, 10, 10 10, 10]] 

137 

138 Parameters 

139 ---------- 

140 structure 

141 Atomic structure. 

142 sublattices 

143 Sublattices for the supercell obtained from the cluster space. 

144 """ 

145 _atoms_on_sublattices = [] 

146 for sl in sublattices: 

147 atoms_on_sublattice = [] 

148 for an in sl.atomic_numbers: 

149 natoms = np.where(structure.numbers == an)[0].size 

150 atoms_on_sublattice.extend([an] * natoms) 

151 _atoms_on_sublattices.append(atoms_on_sublattice) 

152 _atoms_on_sublattices = np.array(_atoms_on_sublattices, 

153 dtype=object) 

154 return _atoms_on_sublattices 

155 

156 

157def get_free_energy_thermodynamic_integration( 

158 dc: DataContainer, 

159 cluster_space: ClusterSpace, 

160 forward: bool, 

161 max_temperature: float = np.inf, 

162 sublattice_probabilities: List[float] = None, 

163 boltzmann_constant: float = kB, 

164) -> (np.ndarray, np.ndarray): 

165 r"""Returns the free energy calculated via thermodynamic integration 

166 using the :class:`ThermodynamicIntegrationEnsemble 

167 <mchammer.ensembles.ThermodynamicIntegrationEnsemble>`. 

168 

169 The temperature dependence of the free energy can be extracted 

170 from the thermodynamic integration as 

171 

172 .. math:: 

173 

174 F(T) = \frac{F_0(\lambda)}{\lambda} + \frac{T_0}{\lambda} S_\text{B} 

175 

176 where :math:`S_\text{B}` is the Boltzmann entropy, 

177 

178 .. math:: 

179 

180 T = \frac{T_0}{\lambda} 

181 

182 and 

183 

184 .. math:: 

185 

186 F_0(\lambda) = \int_0^\lambda \left\langle\frac{\mathrm{d}H(\lambda)} 

187 {\mathrm{d}\lambda}\right\rangle_{H} \mathrm{d}\lambda 

188 

189 Parameters 

190 ---------- 

191 dc 

192 Data container from the thermodynamic integration simulation. 

193 cluster_space 

194 Cluster space used to construct the cluster expansion used for the simulation. 

195 forward 

196 Whether or not the thermodynamic integration was carried out forward or backward. 

197 max_temperature 

198 Largest temperature to extract from the thermodynamic integration. 

199 sublattice_probababilites 

200 Sublattice probabilties that were provided to the thermodynamic integration 

201 simulation. 

202 boltzmann_constant 

203 Boltzmann constant in the units used for the thermodynamic integration 

204 simulation. 

205 

206 """ 

207 

208 lambdas, potentials = dc.get('lambda', 'potential') 

209 if not forward: 

210 # We want to integrate from high to low temperature even though 

211 # the simulation was from low to high. 

212 potentials = potentials[::-1] 

213 lambdas = lambdas[::-1] 

214 

215 sublattices = cluster_space.get_sublattices(dc.structure) 

216 if sublattice_probabilities is None: 

217 sublattice_probabilities = [True] * len(sublattices) 

218 

219 atoms_on_sublattices = _get_atoms_on_sublattice(dc.structure, 

220 sublattices) 

221 ideal_mixing_entropy = _ideal_mixing_entropy(sublattice_probabilities, 

222 atoms_on_sublattices, 

223 boltzmann_constant) 

224 

225 temperature = dc._ensemble_parameters['temperature'] 

226 with np.errstate(divide='ignore'): 

227 # First value of lambdas will be zero (we ignore this warning) 

228 temperatures = (1 / lambdas) * temperature 

229 temp_inds = np.where(temperatures <= max_temperature)[0] 

230 

231 free_energy_change = cumulative_trapezoid(potentials, x=lambdas, initial=0) 

232 with np.errstate(invalid='ignore', divide='ignore'): 

233 # First value of lambdas will be zero (we ignore this warning) 

234 free_energy = (free_energy_change / lambdas - 

235 temperatures * ideal_mixing_entropy) 

236 free_energy[np.isnan(free_energy)] = 0 

237 return (temperatures[temp_inds], free_energy[temp_inds]) 

238 

239 

240def get_free_energy_temperature_integration( 

241 dc: DataContainer, 

242 cluster_space: ClusterSpace, 

243 forward: bool, 

244 temperature_reference: float, 

245 free_energy_reference: float = None, 

246 sublattice_probabilities: List[float] = None, 

247 max_temperature: float = np.inf, 

248 boltzmann_constant: float = kB, 

249) -> (np.ndarray, np.ndarray): 

250 r""" Returns the free energy calculated using temperature integration and 

251 the corresponding temperature. 

252 

253 .. math:: 

254 

255 \frac{A(T_{2})}{T_{2}} = \frac{A(T_{1})}{T_{1}} 

256 - \int_{T_{1}}^{T_{2}}\frac{U(T)}{T^2}\mathrm{d}T 

257 

258 Parameters 

259 ---------- 

260 dc 

261 Data container from a canonical annealing simulation. 

262 The first (last for :attr:`forward=False`) temperature in the 

263 data container has to be at the same temperature as 

264 :attr:`temperature_reference`. 

265 cluster_space 

266 Cluster space used to construct the cluster expansion 

267 that was used in the simulations. 

268 forward 

269 If ``True`` the canonical annealing simulation was carried out 

270 from high to low temperature, otherwise the opposite is assumed. 

271 temperature_reference 

272 Temperature at which :attr:`free_energy_reference` was calculated 

273 free_energy_reference 

274 Reference free energy. If set to ``None`` it will be assumeed that the 

275 free energy at :attr:`temperature_reference` can be approximated by 

276 :math:`T S_B` where :math:`S_B` is the ideal mixing entropy. 

277 sublattice_probababilites 

278 Sublattice probabilties that were provided to the canonical annealing 

279 simulation. 

280 max_temperature 

281 Largest temperature to extract from the temperature integration. 

282 boltzmann_constant 

283 Boltzmann constant in the units used for the thermodynamic integration 

284 simulation. 

285 """ 

286 temperatures, potentials = dc.get('temperature', 'potential') 

287 if not forward: 

288 # We want to integrate from high to low temperature even though 

289 # the simulation was from low to high. 

290 potentials = potentials[::-1] 

291 temperatures = temperatures[::-1] 

292 

293 if not np.isclose(temperatures[0], temperature_reference): 293 ↛ 294line 293 didn't jump to line 294, because the condition on line 293 was never true

294 raise ValueError( 

295 'The first or the last temperature in the temperature list' 

296 ' has to equal the reference temperature.' 

297 f' reference_temperature: {temperature_reference}' 

298 f' first canonical temperature: {temperatures[0]}') 

299 

300 temp_inds = np.where(temperatures <= max_temperature)[0] 

301 

302 if free_energy_reference is None: 302 ↛ 303line 302 didn't jump to line 303, because the condition on line 302 was never true

303 sublattices = cluster_space.get_sublattices(dc.structure) 

304 if sublattice_probabilities is None: 

305 sublattice_probabilities = [True] * len(sublattices) 

306 

307 atoms_on_sublattices = _get_atoms_on_sublattice(dc.structure, 

308 sublattices) 

309 ideal_mixing_entropy = _ideal_mixing_entropy(sublattice_probabilities, 

310 atoms_on_sublattices, 

311 boltzmann_constant) 

312 free_energy_reference = -ideal_mixing_entropy * temperature_reference 

313 

314 reference = free_energy_reference / temperature_reference 

315 integral = cumulative_trapezoid(potentials / temperatures**2, 

316 x=temperatures, initial=0) 

317 free_energy = (reference - integral) * temperatures 

318 return (temperatures[temp_inds], free_energy[temp_inds])