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