Coverage for mchammer/observers/site_occupancy_observer.py: 100%

37 statements  

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

1from ase import Atoms 

2from icet import ClusterSpace 

3from icet.core.structure import Structure 

4from mchammer.observers.base_observer import BaseObserver 

5import numpy as np 

6 

7 

8class SiteOccupancyObserver(BaseObserver): 

9 """ 

10 This class represents a site occupation factor (SOF) observer. 

11 

12 A SOF observer allows to compute the site occupation factors along the 

13 trajectory sampled by a Monte Carlo (MC) simulation. 

14 

15 Parameters 

16 ---------- 

17 cluster_space 

18 Cluster space from which the allowed species are extracted. 

19 structure 

20 Supercell consistent with primitive structure in :attr:`cluster_space`. 

21 Used to determine which species are allowed on each site. 

22 sites 

23 Dictionary containing lists of sites that are to be considered. 

24 The keys will be taken as the names of the sites; the indices refer 

25 to the primitive structure associated with the cluster space. 

26 interval 

27 Observation interval. Defaults to ``None`` meaning that if the 

28 observer is used in a Monte Carlo simulation, then the 

29 :class:`Ensemble` object will determine the interval. 

30 

31 Example 

32 ------- 

33 The following snippet illustrates how to use the site occupancy factor (SOF) 

34 observer in a Monte Carlo simulation of a surface slab. Here, the SOF 

35 observer is used to monitor the concentrations of different species at the 

36 surface, the first subsurface layer, and the remaining 'bulk'. A minimal 

37 cluster expansion is used with slightly modified surface interactions in 

38 order to obtain an example that can be run without much ado. In practice, 

39 one should of course use a proper cluster expansion:: 

40 

41 >>> from ase.build import fcc111 

42 >>> from icet import ClusterExpansion, ClusterSpace 

43 >>> from mchammer.calculators import ClusterExpansionCalculator 

44 >>> from mchammer.ensembles import CanonicalEnsemble 

45 >>> from mchammer.observers import SiteOccupancyObserver 

46 

47 >>> # prepare reference structure 

48 >>> prim = fcc111('Au', size=(1, 1, 10), vacuum=10.0) 

49 >>> prim.translate((0.1, 0.1, 0.0)) 

50 >>> prim.wrap() 

51 >>> prim.pbc = True # icet requires pbc in all directions 

52 

53 >>> # prepare cluster expansion 

54 >>> cs = ClusterSpace(prim, cutoffs=[3.7], chemical_symbols=['Ag', 'Au']) 

55 >>> params = [0] + 5 * [0] + 10 * [0.1] 

56 >>> params[1] = 0.01 

57 >>> params[6] = 0.12 

58 >>> ce = ClusterExpansion(cs, params) 

59 >>> print(ce) 

60 

61 >>> # prepare initial configuration based on a 2x2 supercell 

62 >>> structure = prim.repeat((2, 2, 1)) 

63 >>> for k in range(20): 

64 >>> structure[k].symbol = 'Ag' 

65 

66 >>> # set up MC simulation 

67 >>> calc = ClusterExpansionCalculator(structure, ce) 

68 >>> mc = CanonicalEnsemble(structure=structure, calculator=calc, temperature=600, 

69 ... dc_filename='myrun_sof.dc') 

70 

71 >>> # set up observer and attach it to the MC simulation 

72 >>> sites = {'surface': [0, 9], 'subsurface': [1, 8], 

73 ... 'bulk': list(range(2, 8))} 

74 >>> sof = SiteOccupancyObserver(cs, structure, sites, interval=len(structure)) 

75 >>> mc.attach_observer(sof) 

76 

77 >>> # run 1000 trial steps 

78 >>> mc.run(1000) 

79 

80 After having run this snippet one can access the SOFs via the data 

81 container:: 

82 

83 >>> print(mc.data_container.data) 

84 """ 

85 

86 def __init__(self, cluster_space: ClusterSpace, 

87 structure: Atoms, 

88 sites: dict[str, list[int]], 

89 interval: int = None) -> None: 

90 super().__init__(interval=interval, return_type=dict, tag='SiteOccupancyObserver') 

91 

92 self._sites = {site: sorted(indices) 

93 for site, indices in sites.items()} 

94 

95 self._set_allowed_species(cluster_space, structure) 

96 

97 def _set_allowed_species(self, 

98 cluster_space: ClusterSpace, 

99 structure: Atoms): 

100 """ 

101 Set the allowed species for the selected sites in the Atoms object. 

102 

103 Parameters 

104 ---------- 

105 cluster_space 

106 Cluster space implicitly defining allowed species. 

107 structure 

108 Specific supercell (consistent with cluster_space) whose 

109 allowed species are to be determined. 

110 """ 

111 

112 primitive_structure = Structure.from_atoms(cluster_space.primitive_structure) 

113 chemical_symbols = cluster_space.chemical_symbols 

114 

115 if len(chemical_symbols) == 1: 

116 # If the allowed species are the same for all sites no loop is 

117 # required 

118 allowed_species = {site: chemical_symbols[0] for 

119 site in self._sites.keys()} 

120 else: 

121 # Loop over the lattice sites to find the allowed species 

122 allowed_species = {} 

123 for site, indices in self._sites.items(): 

124 allowed_species[site] = None 

125 positions = structure.positions[np.array(indices)] 

126 lattice_sites = primitive_structure.find_lattice_sites_by_positions( 

127 positions=positions, 

128 fractional_position_tolerance=cluster_space.fractional_position_tolerance) 

129 for k, lattice_site in enumerate(lattice_sites): 

130 species = chemical_symbols[lattice_site.index] 

131 # check that the allowed species are equal for all sites 

132 if allowed_species[site] is not None and \ 

133 species != allowed_species[site]: 

134 raise Exception('The allowed species {} for the site' 

135 ' with index {} differs from the' 

136 ' result {} for the previous index' 

137 ' ({})!'.format(species, indices[k], 

138 allowed_species[site], 

139 indices[k-1])) 

140 allowed_species[site] = species 

141 

142 self._allowed_species = allowed_species 

143 

144 def get_observable(self, structure: Atoms) -> dict[str, list[float]]: 

145 """ 

146 Returns the site occupation factors for a given atomic configuration. 

147 

148 Parameters 

149 ---------- 

150 structure 

151 Input atomic structure. 

152 """ 

153 

154 chemical_symbols = np.array(structure.get_chemical_symbols()) 

155 sofs = {} 

156 for site, indices in self._sites.items(): 

157 counts = {species: 0 for species in self._allowed_species[site]} 

158 symbols, sym_counts = np.unique(chemical_symbols[indices], 

159 return_counts=True) 

160 for sym, count in zip(symbols, sym_counts): 

161 counts[sym] += count 

162 

163 for species in counts.keys(): 

164 key = 'sof_{}_{}'.format(site, species) 

165 sofs[key] = float(counts[species]) / len(indices) 

166 

167 return sofs