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
« 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
9class SwapNotPossibleError(Exception):
10 pass
13class ConfigurationManager(object):
14 """
15 The ConfigurationManager owns and handles information pertaining to a
16 configuration being sampled in a Monte Carlo simulation.
18 Note
19 ----
20 As a user you will usually not interact directly with objects of this type.
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 """
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()
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
50 @property
51 def occupations(self) -> np.ndarray:
52 """ Occupation vector of the configuration (copy). """
53 return self._occupations.copy()
55 @property
56 def sublattices(self) -> Sublattices:
57 """ Sublattices of the configuration. """
58 return self._sublattices
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
67 def get_occupations_on_sublattice(self, sublattice_index: int) -> List[int]:
68 """
69 Returns the occupations on one sublattice.
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])
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.
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
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.
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]]
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))
128 try:
129 site1 = random.choice(available_sites)
130 except IndexError:
131 raise SwapNotPossibleError(f'Sublattice {sublattice_index} is empty.')
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])
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))
149 possible_swap_sites = np.array(possible_swap_sites)
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]))
159 return ([site1, site2], [self._occupations[site2], self._occupations[site1]])
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.
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]]
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))
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
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.
205 Parameters
206 ----------
207 sites
208 Indices of sites of the configuration to change.
209 species
210 New occupations by atomic number.
211 """
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)
222 if new_Z not in self.sublattices[sublattice_index].atomic_numbers:
223 raise ValueError('Invalid new species {} on site {}'.format(new_Z, site))
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))
233 # Update occupation vector itself
234 self._occupations[sites] = species