Coverage for mchammer/configuration_manager.py: 95%
87 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-06 04:14 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-06 04:14 +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 ) -> 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 """
113 # pick the first site
114 if allowed_species is None:
115 available_sites = self.sublattices[sublattice_index].indices
116 else:
117 available_sites = [
118 s for Z in allowed_species for s in
119 self._get_sites_by_species()[sublattice_index][Z]]
121 try:
122 site1 = random.choice(available_sites)
123 except IndexError:
124 raise SwapNotPossibleError(f'Sublattice {sublattice_index} is empty.')
126 # pick the second site
127 if allowed_species is None:
128 possible_swap_species = \
129 set(self._sublattices.get_allowed_numbers_on_site(site1)) - \
130 set([self._occupations[site1]])
131 else:
132 possible_swap_species = \
133 set(allowed_species) - set([self._occupations[site1]])
134 possible_swap_sites = []
135 for Z in possible_swap_species:
136 possible_swap_sites.extend(self._sites_by_species[sublattice_index][Z])
138 possible_swap_sites = np.array(possible_swap_sites)
140 try:
141 site2 = random.choice(possible_swap_sites)
142 except IndexError:
143 raise SwapNotPossibleError(
144 'Cannot swap on sublattice {} since it is full of {} species .'
145 .format(sublattice_index,
146 atomic_number_to_chemical_symbol([self._occupations[site1]])[0]))
148 return ([site1, site2], [self._occupations[site2], self._occupations[site1]])
150 def get_flip_state(
151 self,
152 sublattice_index: int,
153 allowed_species: List[int] = None,
154 ) -> Tuple[int, int]:
155 """
156 Returns a site index and a new species for the site.
158 Parameters
159 ----------
160 sublattice_index
161 Index of sublattice from which to pick a site.
162 allowed_species
163 List of atomic numbers for allowed species.
164 """
165 if allowed_species is None:
166 available_sites = self._sublattices[sublattice_index].indices
167 else:
168 available_sites = [s for Z in allowed_species for s in
169 self._get_sites_by_species()[sublattice_index][Z]]
171 site = random.choice(available_sites)
172 if allowed_species is not None:
173 species = random.choice(list(
174 set(allowed_species) - set([self._occupations[site]])))
175 else:
176 species = random.choice(list(
177 set(self._sublattices[sublattice_index].atomic_numbers) -
178 set([self._occupations[site]])))
179 return site, species
181 def update_occupations(self, sites: List[int], species: List[int]) -> None:
182 """
183 Updates the occupation vector of the configuration being sampled.
184 This will change the state in both the configuration in the calculator
185 and the configuration manager.
187 Parameters
188 ----------
189 sites
190 Indices of sites of the configuration to change.
191 species
192 New occupations by atomic number.
193 """
195 # Update sublattices
196 for site, new_Z in zip(sites, species):
197 if 0 > new_Z > 118: 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true
198 raise ValueError('Invalid new species {} on site {}'.format(new_Z, site))
199 if len(self._occupations) >= site < 0:
200 raise ValueError('Site {} is not a valid site index'.format(site))
201 old_Z = self._occupations[site]
202 sublattice_index = self.sublattices.get_sublattice_index_from_site_index(site)
204 if new_Z not in self.sublattices[sublattice_index].atomic_numbers:
205 raise ValueError('Invalid new species {} on site {}'.format(new_Z, site))
207 # Remove site from list of sites for old species
208 self._sites_by_species[sublattice_index][old_Z].remove(site)
209 # Add site to list of sites for new species
210 try:
211 self._sites_by_species[sublattice_index][new_Z].append(site)
212 except KeyError:
213 raise ValueError('Invalid new species {} on site {}'.format(new_Z, site))
215 # Update occupation vector itself
216 self._occupations[sites] = species