Coverage for mchammer/configuration_manager.py: 90%

92 statements  

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

1import random 

2import numpy as np 

3from ase import Atoms 

4from icet.core.sublattices import Sublattices 

5from icet.tools.geometry import atomic_number_to_chemical_symbol 

6 

7 

8class SwapNotPossibleError(Exception): 

9 pass 

10 

11 

12class ConfigurationManager(object): 

13 """ 

14 The ConfigurationManager owns and handles information pertaining to a 

15 configuration being sampled in a Monte Carlo simulation. 

16 

17 Note 

18 ---- 

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

20 

21 Parameters 

22 ---------- 

23 structure : Atoms 

24 Configuration to be handled. 

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

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

27 """ 

28 

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

30 self._structure = structure.copy() 

31 self._occupations = self._structure.numbers 

32 self._sublattices = sublattices 

33 self._sites_by_species = self._get_sites_by_species() 

34 

35 def _get_sites_by_species(self) -> list[dict[int, list[int]]]: 

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

37 dictionary represents one sublattice where the key is the 

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

39 occupied by said species in the respective sublattice. 

40 """ 

41 sites_by_species = [] 

42 for sl in self._sublattices: 

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

44 for site in sl.indices: 

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

46 sites_by_species.append(species_dict) 

47 return sites_by_species 

48 

49 @property 

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

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

52 return self._occupations.copy() 

53 

54 @property 

55 def sublattices(self) -> Sublattices: 

56 """ Sublattices of the configuration. """ 

57 return self._sublattices 

58 

59 @property 

60 def structure(self) -> Atoms: 

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

62 structure = self._structure.copy() 

63 structure.set_atomic_numbers(self.occupations) 

64 return structure 

65 

66 def get_occupations_on_sublattice(self, sublattice_index: int) -> list[int]: 

67 """ 

68 Returns the occupations on one sublattice. 

69 

70 Parameters 

71 --------- 

72 sublattice_index 

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

74 """ 

75 sl = self.sublattices[sublattice_index] 

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

77 

78 def is_swap_possible(self, sublattice_index: int, 

79 allowed_species: list[int] = None) -> bool: 

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

81 

82 Parameters 

83 ---------- 

84 sublattice_index 

85 Index of sublattice to be checked. 

86 allowed_species 

87 List of atomic numbers for allowed species. 

88 """ 

89 sl = self.sublattices[sublattice_index] 

90 if allowed_species is None: 

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

92 else: 

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

94 allowed_species]) 

95 return len(swap_symbols) > 1 

96 

97 def get_swapped_state(self, sublattice_index: int, 

98 allowed_species: list[int] = None, 

99 allowed_sites: list[int] = None 

100 ) -> tuple[list[int], list[int]]: 

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

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

103 configuration will obey the occupation constraints associated 

104 with the :class:`ConfigurationManager` object. 

105 

106 Parameters 

107 ---------- 

108 sublattice_index 

109 Sublattice by index from which to pick sites. 

110 allowed_species 

111 List of atomic numbers for allowed species. 

112 allowed_sites 

113 List of indices for allowed sites. 

114 """ 

115 # pick the first site 

116 if allowed_species is None: 

117 available_sites = self.sublattices[sublattice_index].indices 

118 else: 

119 available_sites = [ 

120 s for Z in allowed_species for s in 

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

122 

123 # only include allowed sites 

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

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

126 

127 try: 

128 site1 = random.choice(available_sites) 

129 except IndexError: 

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

131 

132 # pick the second site 

133 if allowed_species is None: 

134 possible_swap_species = \ 

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

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

137 else: 

138 possible_swap_species = \ 

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

140 possible_swap_sites = [] 

141 for Z in possible_swap_species: 

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

143 

144 # only include allowed sites 

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

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

147 

148 possible_swap_sites = np.array(possible_swap_sites) 

149 

150 try: 

151 site2 = random.choice(possible_swap_sites) 

152 except IndexError: 

153 raise SwapNotPossibleError( 

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

155 .format(sublattice_index, 

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

157 

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

159 

160 def get_flip_state( 

161 self, 

162 sublattice_index: int, 

163 allowed_species: list[int] = None, 

164 allowed_sites: list[int] = None 

165 ) -> tuple[int, int]: 

166 """ 

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

168 

169 Parameters 

170 ---------- 

171 sublattice_index 

172 Index of sublattice from which to pick a site. 

173 allowed_species 

174 List of atomic numbers for allowed species. 

175 allowed_sites 

176 List of indices for allowed sites. 

177 """ 

178 if allowed_species is None: 

179 available_sites = self._sublattices[sublattice_index].indices 

180 else: 

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

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

183 

184 # only include allowed sites 

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

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

187 

188 site = random.choice(available_sites) 

189 if allowed_species is not None: 

190 species = random.choice(list( 

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

192 else: 

193 species = random.choice(list( 

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

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

196 return site, species 

197 

198 def update_occupations(self, sites: list[int], species: list[int]) -> None: 

199 """ 

200 Updates the occupation vector of the configuration being sampled. 

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

202 and the configuration manager. 

203 

204 Parameters 

205 ---------- 

206 sites 

207 Indices of sites of the configuration to change. 

208 species 

209 New occupations by atomic number. 

210 """ 

211 

212 # Update sublattices 

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

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

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

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

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

218 old_Z = self._occupations[site] 

219 sublattice_index = self.sublattices.get_sublattice_index_from_site_index(site) 

220 

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

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

223 

224 # Remove site from list of sites for old species 

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

226 # Add site to list of sites for new species 

227 try: 

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

229 except KeyError: 

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

231 

232 # Update occupation vector itself 

233 self._occupations[sites] = species