Coverage for mchammer/ensembles/canonical_annealing.py: 93%

69 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-09-14 04:08 +0000

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

2 

3import numpy as np 

4 

5from ase import Atoms 

6from ase.units import kB 

7 

8from .. import DataContainer 

9from ..calculators import ClusterExpansionCalculator 

10from .thermodynamic_base_ensemble import ThermodynamicBaseEnsemble 

11 

12 

13class CanonicalAnnealing(ThermodynamicBaseEnsemble): 

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

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

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

17 :class:`CanonicalEnsemble <mchammer.ensembles.CanonicalEnsemble>` 

18 for more information about the standard canonical ensemble. 

19 

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

21 finding ground states or generating low energy configurations. 

22 

23 The temperature control scheme is selected via the 

24 :attr:`cooling_function` keyword argument, while the initial and final 

25 temperature are set via the :attr:`T_start` and :attr:`T_stop` arguments. 

26 Several pre-defined temperature control schemes are available 

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

28 temperature varies logarithmatically as a function of the MC step, 

29 emulating the exponential temperature dependence of the atomic 

30 exchange rate encountered in many materials. It is also possible 

31 to provide a user defined cooling function via the keyword 

32 argument. This function must comply with the following function 

33 header:: 

34 

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

36 T = ... # compute temperature 

37 return T 

38 

39 Here :attr:`step` refers to the current MC trial step. 

40 

41 Parameters 

42 ---------- 

43 structure 

44 Atomic configuration to be used in the Monte Carlo simulation; 

45 also defines the initial occupation vector. 

46 calculator 

47 Calculator to be used for calculating the potential changes 

48 that enter the evaluation of the Metropolis criterion. 

49 T_start 

50 Temperature from which the annealing is started. 

51 T_stop 

52 Final temperature for annealing. 

53 n_steps 

54 Number of steps to take in the annealing simulation. 

55 cooling_function 

56 to use the predefined cooling functions provide a string 

57 ``'linear'`` or ``'exponential'``, otherwise provide a function. 

58 boltzmann_constant 

59 Boltzmann constant :math:`k_B` in appropriate 

60 units, i.e., units that are consistent 

61 with the underlying cluster expansion 

62 and the temperature units. Default: eV/K. 

63 user_tag 

64 Human-readable tag for ensemble. Default: ``None``. 

65 random_seed 

66 Seed for the random number generator used in the Monte Carlo simulation. 

67 dc_filename 

68 Name of file the data container associated with the ensemble 

69 will be written to. If the file exists it will be read, the 

70 data container will be appended, and the file will be 

71 updated/overwritten. 

72 data_container_write_period 

73 Period in units of seconds at which the data container is 

74 written to file. Writing periodically to file provides both 

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

76 the data. Default: 600 s. 

77 ensemble_data_write_interval 

78 interval at which data is written to the data container. This 

79 includes for example the current value of the calculator 

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

81 such as temperature or the number of atoms of different species. 

82 Default: Number of sites in the :attr:`structure`. 

83 trajectory_write_interval 

84 interval at which the current occupation vector of the atomic 

85 configuration is written to the data container. 

86 Default: Number of sites in the :attr:`structure`. 

87 sublattice_probabilities 

88 Probability for picking a sublattice when doing a random swap. 

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

90 sum up to 1. 

91 

92 """ 

93 

94 def __init__(self, 

95 structure: Atoms, 

96 calculator: ClusterExpansionCalculator, 

97 T_start: float, 

98 T_stop: float, 

99 n_steps: int, 

100 cooling_function: str = 'exponential', 

101 user_tag: str = None, 

102 boltzmann_constant: float = kB, 

103 random_seed: int = None, 

104 dc_filename: str = None, 

105 data_container_write_period: float = 600, 

106 ensemble_data_write_interval: int = None, 

107 trajectory_write_interval: int = None, 

108 sublattice_probabilities: list[float] = None) -> None: 

109 

110 self._ensemble_parameters = dict(n_steps=n_steps) 

111 

112 # add species count to ensemble parameters 

113 for sl in calculator.sublattices: 

114 for symbol in sl.chemical_symbols: 

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

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

117 self._ensemble_parameters[key] = count 

118 

119 super().__init__( 

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

121 random_seed=random_seed, 

122 dc_filename=dc_filename, 

123 data_container_class=DataContainer, 

124 data_container_write_period=data_container_write_period, 

125 ensemble_data_write_interval=ensemble_data_write_interval, 

126 trajectory_write_interval=trajectory_write_interval, 

127 boltzmann_constant=boltzmann_constant) 

128 

129 self._temperature = T_start 

130 self._T_start = T_start 

131 self._T_stop = T_stop 

132 self._n_steps = n_steps 

133 

134 self._ground_state_candidate = self.configuration.structure 

135 self._ground_state_candidate_potential = calculator.calculate_total( 

136 occupations=self.configuration.occupations) 

137 

138 # setup cooling function 

139 if isinstance(cooling_function, str): 

140 available = sorted(available_cooling_functions.keys()) 

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

142 raise ValueError( 

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

144 self._cooling_function = available_cooling_functions[cooling_function] 

145 elif callable(cooling_function): 145 ↛ 148line 145 didn't jump to line 148 because the condition on line 145 was always true

146 self._cooling_function = cooling_function 

147 else: 

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

149 

150 if sublattice_probabilities is None: 150 ↛ 153line 150 didn't jump to line 153 because the condition on line 150 was always true

151 self._swap_sublattice_probabilities = self._get_swap_sublattice_probabilities() 

152 else: 

153 self._swap_sublattice_probabilities = sublattice_probabilities 

154 

155 @property 

156 def temperature(self) -> float: 

157 """ Current temperature. """ 

158 return self._temperature 

159 

160 @property 

161 def T_start(self) -> float: 

162 """ Starting temperature. """ 

163 return self._T_start 

164 

165 @property 

166 def T_stop(self) -> float: 

167 """ Starting temperature """ 

168 return self._T_stop 

169 

170 @property 

171 def n_steps(self) -> int: 

172 """ Number of steps to carry out. """ 

173 return self._n_steps 

174 

175 @property 

176 def estimated_ground_state(self): 

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

178 return self._ground_state_candidate.copy() 

179 

180 @property 

181 def estimated_ground_state_potential(self): 

182 """ Lowest observed potential during run. """ 

183 return self._ground_state_candidate_potential 

184 

185 def run(self): 

186 """ Runs the annealing simulation. """ 

187 if self.step >= self.n_steps: 

188 raise Exception('Annealing has already finished') 

189 super().run(self.n_steps - self.step) 

190 

191 def _do_trial_step(self) -> int: 

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

193 self._temperature = self._cooling_function( 

194 self.step, self.T_start, self.T_stop, self.n_steps) 

195 sublattice_index = self.get_random_sublattice_index(self._swap_sublattice_probabilities) 

196 return self.do_canonical_swap(sublattice_index=sublattice_index) 

197 

198 def _get_ensemble_data(self) -> dict: 

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

200 CanonicalAnnealing this specifically includes the temperature. 

201 """ 

202 data = super()._get_ensemble_data() 

203 data['temperature'] = self.temperature 

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

205 self._ground_state_candidate_potential = data['potential'] 

206 self._ground_state_candidate = self.configuration.structure 

207 return data 

208 

209 

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

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

212 

213 

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

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

216 

217 

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