Coverage for mchammer/configuration_manager.py: 91%

93 statements  

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

1import random 

2import numpy as np 

3from typing import Dict, List, Tuple 

4from ase import Atoms 

5from icet.core.sublattices import Sublattices 

6from icet.tools.geometry import atomic_number_to_chemical_symbol 

7 

8 

9class SwapNotPossibleError(Exception): 

10 pass 

11 

12 

13class ConfigurationManager(object): 

14 """ 

15 The ConfigurationManager owns and handles information pertaining to a 

16 configuration being sampled in a Monte Carlo simulation. 

17 

18 Note 

19 ---- 

20 As a user you will usually not interact directly with objects of this type. 

21 

22 Parameters 

23 ---------- 

24 structure : Atoms 

25 Configuration to be handled. 

26 sublattices : :class:`Sublattices <icet.core.sublattices.Sublattices>` 

27 Sublattices used to define allowed occupations and handle related information. 

28 """ 

29 

30 def __init__(self, structure: Atoms, sublattices: Sublattices) -> None: 

31 self._structure = structure.copy() 

32 self._occupations = self._structure.numbers 

33 self._sublattices = sublattices 

34 self._sites_by_species = self._get_sites_by_species() 

35 

36 def _get_sites_by_species(self) -> List[Dict[int, List[int]]]: 

37 """Returns the sites that are occupied for each species. Each 

38 dictionary represents one sublattice where the key is the 

39 species (by atomic number) and the value is the list of sites 

40 occupied by said species in the respective sublattice. 

41 """ 

42 sites_by_species = [] 

43 for sl in self._sublattices: 

44 species_dict = {key: [] for key in sl.atomic_numbers} 

45 for site in sl.indices: 

46 species_dict[self._occupations[site]].append(site) 

47 sites_by_species.append(species_dict) 

48 return sites_by_species 

49 

50 @property 

51 def occupations(self) -> np.ndarray: 

52 """ Occupation vector of the configuration (copy). """ 

53 return self._occupations.copy() 

54 

55 @property 

56 def sublattices(self) -> Sublattices: 

57 """ Sublattices of the configuration. """ 

58 return self._sublattices 

59 

60 @property 

61 def structure(self) -> Atoms: 

62 """ Atomic structure associated with configuration (copy). """ 

63 structure = self._structure.copy() 

64 structure.set_atomic_numbers(self.occupations) 

65 return structure 

66 

67 def get_occupations_on_sublattice(self, sublattice_index: int) -> List[int]: 

68 """ 

69 Returns the occupations on one sublattice. 

70 

71 Parameters 

72 --------- 

73 sublattice_index 

74 Sublattice by index for which the occupations should be returned. 

75 """ 

76 sl = self.sublattices[sublattice_index] 

77 return list(self.occupations[sl.indices]) 

78 

79 def is_swap_possible(self, sublattice_index: int, 

80 allowed_species: List[int] = None) -> bool: 

81 """ Checks if a swap trial move is possible on a specific sublattice. 

82 

83 Parameters 

84 ---------- 

85 sublattice_index 

86 Index of sublattice to be checked. 

87 allowed_species 

88 List of atomic numbers for allowed species. 

89 """ 

90 sl = self.sublattices[sublattice_index] 

91 if allowed_species is None: 

92 swap_symbols = set(self.occupations[sl.indices]) 

93 else: 

94 swap_symbols = set([o for o in self.occupations[sl.indices] if o in 

95 allowed_species]) 

96 return len(swap_symbols) > 1 

97 

98 def get_swapped_state(self, sublattice_index: int, 

99 allowed_species: List[int] = None, 

100 allowed_sites: List[int] = None 

101 ) -> Tuple[List[int], List[int]]: 

102 """Returns two random sites (first element of tuple) and their 

103 occupation after a swap (second element of tuple). The new 

104 configuration will obey the occupation constraints associated 

105 with the :class:`ConfigurationManager` object. 

106 

107 Parameters 

108 ---------- 

109 sublattice_index 

110 Sublattice by index from which to pick sites. 

111 allowed_species 

112 List of atomic numbers for allowed species. 

113 allowed_sites 

114 List of indices for allowed sites. 

115 """ 

116 # pick the first site 

117 if allowed_species is None: 

118 available_sites = self.sublattices[sublattice_index].indices 

119 else: 

120 available_sites = [ 

121 s for Z in allowed_species for s in 

122 self._get_sites_by_species()[sublattice_index][Z]] 

123 

124 # only include allowed sites 

125 if allowed_sites is not None: 125 ↛ 126line 125 didn't jump to line 126, because the condition on line 125 was never true

126 available_sites = list(set(available_sites).intersection(allowed_sites)) 

127 

128 try: 

129 site1 = random.choice(available_sites) 

130 except IndexError: 

131 raise SwapNotPossibleError(f'Sublattice {sublattice_index} is empty.') 

132 

133 # pick the second site 

134 if allowed_species is None: 

135 possible_swap_species = \ 

136 set(self._sublattices.get_allowed_numbers_on_site(site1)) - \ 

137 set([self._occupations[site1]]) 

138 else: 

139 possible_swap_species = \ 

140 set(allowed_species) - set([self._occupations[site1]]) 

141 possible_swap_sites = [] 

142 for Z in possible_swap_species: 

143 possible_swap_sites.extend(self._sites_by_species[sublattice_index][Z]) 

144 

145 # only include allowed sites 

146 if allowed_sites is not None: 146 ↛ 147line 146 didn't jump to line 147, because the condition on line 146 was never true

147 possible_swap_sites = list(set(possible_swap_sites).intersection(allowed_sites)) 

148 

149 possible_swap_sites = np.array(possible_swap_sites) 

150 

151 try: 

152 site2 = random.choice(possible_swap_sites) 

153 except IndexError: 

154 raise SwapNotPossibleError( 

155 'Cannot swap on sublattice {} since it is full of {} species .' 

156 .format(sublattice_index, 

157 atomic_number_to_chemical_symbol([self._occupations[site1]])[0])) 

158 

159 return ([site1, site2], [self._occupations[site2], self._occupations[site1]]) 

160 

161 def get_flip_state( 

162 self, 

163 sublattice_index: int, 

164 allowed_species: List[int] = None, 

165 allowed_sites: List[int] = None 

166 ) -> Tuple[int, int]: 

167 """ 

168 Returns a site index and a new species for the site. 

169 

170 Parameters 

171 ---------- 

172 sublattice_index 

173 Index of sublattice from which to pick a site. 

174 allowed_species 

175 List of atomic numbers for allowed species. 

176 allowed_sites 

177 List of indices for allowed sites. 

178 """ 

179 if allowed_species is None: 

180 available_sites = self._sublattices[sublattice_index].indices 

181 else: 

182 available_sites = [s for Z in allowed_species for s in 

183 self._get_sites_by_species()[sublattice_index][Z]] 

184 

185 # only include allowed sites 

186 if allowed_sites is not None: 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never true

187 available_sites = list(set(available_sites).intersection(allowed_sites)) 

188 

189 site = random.choice(available_sites) 

190 if allowed_species is not None: 

191 species = random.choice(list( 

192 set(allowed_species) - set([self._occupations[site]]))) 

193 else: 

194 species = random.choice(list( 

195 set(self._sublattices[sublattice_index].atomic_numbers) - 

196 set([self._occupations[site]]))) 

197 return site, species 

198 

199 def update_occupations(self, sites: List[int], species: List[int]) -> None: 

200 """ 

201 Updates the occupation vector of the configuration being sampled. 

202 This will change the state in both the configuration in the calculator 

203 and the configuration manager. 

204 

205 Parameters 

206 ---------- 

207 sites 

208 Indices of sites of the configuration to change. 

209 species 

210 New occupations by atomic number. 

211 """ 

212 

213 # Update sublattices 

214 for site, new_Z in zip(sites, species): 

215 if 0 > new_Z > 118: 215 ↛ 216line 215 didn't jump to line 216, because the condition on line 215 was never true

216 raise ValueError('Invalid new species {} on site {}'.format(new_Z, site)) 

217 if len(self._occupations) >= site < 0: 

218 raise ValueError('Site {} is not a valid site index'.format(site)) 

219 old_Z = self._occupations[site] 

220 sublattice_index = self.sublattices.get_sublattice_index_from_site_index(site) 

221 

222 if new_Z not in self.sublattices[sublattice_index].atomic_numbers: 

223 raise ValueError('Invalid new species {} on site {}'.format(new_Z, site)) 

224 

225 # Remove site from list of sites for old species 

226 self._sites_by_species[sublattice_index][old_Z].remove(site) 

227 # Add site to list of sites for new species 

228 try: 

229 self._sites_by_species[sublattice_index][new_Z].append(site) 

230 except KeyError: 

231 raise ValueError('Invalid new species {} on site {}'.format(new_Z, site)) 

232 

233 # Update occupation vector itself 

234 self._occupations[sites] = species