Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

"""Definition of the canonical annealing class.""" 

 

import numpy as np 

 

from ase import Atoms 

from ase.units import kB 

from typing import Dict, List 

 

from .. import DataContainer 

from ..calculators.base_calculator import BaseCalculator 

from .thermodynamic_base_ensemble import ThermodynamicBaseEnsemble 

 

 

class CanonicalAnnealing(ThermodynamicBaseEnsemble): 

"""Instances of this class allow one to carry out simulated annealing 

in the canonical ensemble, i.e. the temperature is varied in 

pre-defined fashion while the composition is kept fixed. See 

:class:`mchammer.ensembles.CanonicalEnsemble` for more information 

about the standard canonical ensemble. 

 

The canonical annealing ensemble can be useful, for example, for 

finding ground states or generating low energy configurations. 

 

The temperature control scheme is selected via the 

``cooling_function`` keyword argument, while the initial and final 

temperature are set via the ``T_start`` and ``T_stop`` arguments. 

Several pre-defined temperature control schemes are available 

including `linear` and `exponential`. In the latter case the 

temperature varies logarithmatically as a function of the MC step, 

emulating the exponential temperature dependence of the atomic 

exchange rate encountered in many materials. It is also possible 

to provide a user defined cooling function via the keyword 

argument. This function must comply with the following function 

header:: 

 

def cooling_function(step, T_start, T_stop, n_steps): 

T = ... # compute temperature 

return T 

 

Here ``step`` refers to the current MC trial step. 

 

Parameters 

---------- 

structure : :class:`Atoms <ase.Atoms>` 

atomic configuration to be used in the Monte Carlo simulation; 

also defines the initial occupation vector 

calculator : :class:`BaseCalculator <mchammer.calculators.ClusterExpansionCalculator>` 

calculator to be used for calculating the potential changes 

that enter the evaluation of the Metropolis criterion 

T_start : float 

temperature from which the annealing is started 

T_stop : float 

final temperature for annealing 

n_steps : int 

number of steps to take in the annealing simulation 

cooling_function : str/function 

to use the predefined cooling functions provide a string 

`linear` or `exponential`, otherwise provide a function 

boltzmann_constant : float 

Boltzmann constant :math:`k_B` in appropriate 

units, i.e. units that are consistent 

with the underlying cluster expansion 

and the temperature units [default: eV/K] 

user_tag : str 

human-readable tag for ensemble [default: None] 

data_container : str 

name of file the data container associated with the ensemble 

will be written to; if the file exists it will be read, the 

data container will be appended, and the file will be 

updated/overwritten 

random_seed : int 

seed for the random number generator used in the Monte Carlo 

simulation 

ensemble_data_write_interval : int 

interval at which data is written to the data container; this 

includes for example the current value of the calculator 

(i.e. usually the energy) as well as ensembles specific fields 

such as temperature or the number of atoms of different species 

data_container_write_period : float 

period in units of seconds at which the data container is 

written to file; writing periodically to file provides both 

a way to examine the progress of the simulation and to back up 

the data [default: np.inf] 

trajectory_write_interval : int 

interval at which the current occupation vector of the atomic 

configuration is written to the data container. 

sublattice_probabilities : List[float] 

probability for picking a sublattice when doing a random swap. 

This should be as long as the number of sublattices and should 

sum up to 1. 

 

""" 

 

def __init__(self, structure: Atoms, calculator: BaseCalculator, 

T_start: float, T_stop: float, n_steps: int, 

cooling_function: str = 'exponential', 

user_tag: str = None, 

boltzmann_constant: float = kB, 

data_container: DataContainer = None, random_seed: int = None, 

data_container_write_period: float = np.inf, 

ensemble_data_write_interval: int = None, 

trajectory_write_interval: int = None, 

sublattice_probabilities: List[float] = None) -> None: 

 

self._ensemble_parameters = dict(n_steps=n_steps) 

 

# add species count to ensemble parameters 

for sl in calculator.sublattices: 

for symbol in sl.chemical_symbols: 

key = 'n_atoms_{}'.format(symbol) 

count = structure.get_chemical_symbols().count(symbol) 

self._ensemble_parameters[key] = count 

 

super().__init__( 

structure=structure, calculator=calculator, user_tag=user_tag, 

data_container=data_container, 

random_seed=random_seed, 

data_container_write_period=data_container_write_period, 

ensemble_data_write_interval=ensemble_data_write_interval, 

trajectory_write_interval=trajectory_write_interval, 

boltzmann_constant=boltzmann_constant) 

 

self._temperature = T_start 

self._T_start = T_start 

self._T_stop = T_stop 

self._n_steps = n_steps 

 

self._ground_state_candidate = self.configuration.structure 

self._ground_state_candidate_potential = self.calculator.calculate_total( 

occupations=self.configuration.occupations) 

 

# setup cooling function 

if isinstance(cooling_function, str): 

available = sorted(available_cooling_functions.keys()) 

135 ↛ 136line 135 didn't jump to line 136, because the condition on line 135 was never true if cooling_function not in available: 

raise ValueError( 

'Select from the available cooling_functions {}'.format(available)) 

self._cooling_function = available_cooling_functions[cooling_function] 

139 ↛ 142line 139 didn't jump to line 142, because the condition on line 139 was never false elif callable(cooling_function): 

self._cooling_function = cooling_function 

else: 

raise TypeError('cooling_function must be either str or a function') 

 

144 ↛ 147line 144 didn't jump to line 147, because the condition on line 144 was never false if sublattice_probabilities is None: 

self._swap_sublattice_probabilities = self._get_swap_sublattice_probabilities() 

else: 

self._swap_sublattice_probabilities = sublattice_probabilities 

 

@property 

def temperature(self) -> float: 

""" Current temperature """ 

return self._temperature 

 

@property 

def T_start(self) -> float: 

""" Starting temperature """ 

return self._T_start 

 

@property 

def T_stop(self) -> float: 

""" Starting temperature """ 

return self._T_stop 

 

@property 

def n_steps(self) -> int: 

""" Number of steps to carry out """ 

return self._n_steps 

 

@property 

def estimated_ground_state(self): 

""" Structure with lowest observed potential during run """ 

return self._ground_state_candidate.copy() 

 

@property 

def estimated_ground_state_potential(self): 

""" Lowest observed potential during run """ 

return self._ground_state_candidate_potential 

 

def run(self): 

""" Runs the annealing. """ 

if self.total_trials >= self.n_steps: 

raise Exception('Annealing has already finished') 

super().run(self.n_steps - self.total_trials) 

 

def _do_trial_step(self): 

""" Carries out one Monte Carlo trial step. """ 

self._temperature = self._cooling_function( 

self.total_trials, self.T_start, self.T_stop, self.n_steps) 

sublattice_index = self.get_random_sublattice_index(self._swap_sublattice_probabilities) 

self.do_canonical_swap(sublattice_index=sublattice_index) 

 

def _get_ensemble_data(self) -> Dict: 

"""Returns the data associated with the ensemble. For the 

CanonicalAnnealing this specifically includes the temperature. 

""" 

data = super()._get_ensemble_data() 

data['temperature'] = self.temperature 

if data['potential'] < self._ground_state_candidate_potential: 

self._ground_state_candidate_potential = data['potential'] 

self._ground_state_candidate = self.configuration.structure 

return data 

 

 

def _cooling_linear(step, T_start, T_stop, n_steps): 

return T_start + (T_stop-T_start) * step / (n_steps - 1) 

 

 

def _cooling_exponential(step, T_start, T_stop, n_steps): 

return T_start - (T_start - T_stop) * np.log(step+1) / np.log(n_steps) 

 

 

available_cooling_functions = dict(linear=_cooling_linear, exponential=_cooling_exponential)