Coverage for /builds/ase/ase/ase/ga/particle_crossovers.py: 73.91%

115 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3from itertools import chain 

4 

5import numpy as np 

6 

7from ase import Atoms 

8from ase.ga.offspring_creator import OffspringCreator 

9 

10 

11class Crossover(OffspringCreator): 

12 """Base class for all particle crossovers. 

13 

14 Originally intended for medium sized particles 

15 

16 Do not call this class directly.""" 

17 

18 def __init__(self, rng=np.random): 

19 OffspringCreator.__init__(self, rng=rng) 

20 self.descriptor = 'Crossover' 

21 self.min_inputs = 2 

22 

23 

24class CutSpliceCrossover(Crossover): 

25 """Crossover that cuts two particles through a plane in space and 

26 merges two halfes from different particles together. 

27 

28 Implementation of the method presented in: 

29 D. M. Deaven and K. M. Ho, Phys. Rev. Lett., 75, 2, 288-291 (1995) 

30 

31 It keeps the correct composition by randomly assigning elements in 

32 the new particle. If some of the atoms in the two particle halves 

33 are too close, the halves are moved away from each other perpendicular 

34 to the cutting plane. 

35 

36 Parameters: 

37 

38 blmin: dictionary of minimum distance between atomic numbers. 

39 e.g. {(28,29): 1.5} 

40 

41 keep_composition: boolean that signifies if the composition should 

42 be the same as in the parents. 

43 

44 rng: Random number generator 

45 By default numpy.random. 

46 """ 

47 

48 def __init__(self, blmin, keep_composition=True, rng=np.random): 

49 Crossover.__init__(self, rng=rng) 

50 self.blmin = blmin 

51 self.keep_composition = keep_composition 

52 self.descriptor = 'CutSpliceCrossover' 

53 

54 def get_new_individual(self, parents): 

55 f, m = parents 

56 

57 indi = self.initialize_individual(f) 

58 indi.info['data']['parents'] = [i.info['confid'] for i in parents] 

59 

60 theta = self.rng.random() * 2 * np.pi # 0,2pi 

61 phi = self.rng.random() * np.pi # 0,pi 

62 e = np.array((np.sin(phi) * np.cos(theta), 

63 np.sin(theta) * np.sin(phi), 

64 np.cos(phi))) 

65 eps = 0.0001 

66 

67 f.translate(-f.get_center_of_mass()) 

68 m.translate(-m.get_center_of_mass()) 

69 

70 # Get the signed distance to the cutting plane 

71 # We want one side from f and the other side from m 

72 fmap = [np.dot(x, e) for x in f.get_positions()] 

73 mmap = [-np.dot(x, e) for x in m.get_positions()] 

74 ain = sorted([i for i in chain(fmap, mmap) if i > 0], 

75 reverse=True) 

76 aout = sorted([i for i in chain(fmap, mmap) if i < 0], 

77 reverse=True) 

78 

79 off = len(ain) - len(f) 

80 

81 # Translating f and m to get the correct number of atoms 

82 # in the offspring 

83 if off < 0: 

84 # too few 

85 # move f and m away from the plane 

86 dist = (abs(aout[abs(off) - 1]) + abs(aout[abs(off)])) * .5 

87 f.translate(e * dist) 

88 m.translate(-e * dist) 

89 elif off > 0: 

90 # too many 

91 # move f and m towards the plane 

92 dist = (abs(ain[-off - 1]) + abs(ain[-off])) * .5 

93 f.translate(-e * dist) 

94 m.translate(e * dist) 

95 if off != 0 and dist == 0: 

96 # Exactly same position => we continue with the wrong number 

97 # of atoms. What should be done? Fail or return None or 

98 # remove one of the two atoms with exactly the same position. 

99 pass 

100 

101 # Determine the contributing parts from f and m 

102 tmpf, tmpm = Atoms(), Atoms() 

103 for atom in f: 

104 if np.dot(atom.position, e) > 0: 

105 atom.tag = 1 

106 tmpf.append(atom) 

107 for atom in m: 

108 if np.dot(atom.position, e) < 0: 

109 atom.tag = 2 

110 tmpm.append(atom) 

111 

112 # Check that the correct composition is employed 

113 if self.keep_composition: 

114 opt_sm = sorted(f.numbers) 

115 tmpf_numbers = list(tmpf.numbers) 

116 tmpm_numbers = list(tmpm.numbers) 

117 cur_sm = sorted(tmpf_numbers + tmpm_numbers) 

118 # correct_by: dictionary that specifies how many 

119 # of the atom_numbers should be removed (a negative number) 

120 # or added (a positive number) 

121 correct_by = {j: opt_sm.count(j) for j in set(opt_sm)} 

122 for n in cur_sm: 

123 correct_by[n] -= 1 

124 correct_in = tmpf if self.rng.choice([0, 1]) else tmpm 

125 to_add, to_rem = [], [] 

126 for num, amount in correct_by.items(): 

127 if amount > 0: 

128 to_add.extend([num] * amount) 

129 elif amount < 0: 

130 to_rem.extend([num] * abs(amount)) 

131 for add, rem in zip(to_add, to_rem): 

132 tbc = [a.index for a in correct_in if a.number == rem] 

133 if len(tbc) == 0: 

134 pass 

135 ai = self.rng.choice(tbc) 

136 correct_in[ai].number = add 

137 

138 # Move the contributing apart if any distance is below blmin 

139 maxl = 0. 

140 for sv, min_dist in self.get_vectors_below_min_dist(tmpf + tmpm): 

141 lsv = np.linalg.norm(sv) # length of shortest vector 

142 d = [-np.dot(e, sv)] * 2 

143 d[0] += np.sqrt(np.dot(e, sv)**2 - lsv**2 + min_dist**2) 

144 d[1] -= np.sqrt(np.dot(e, sv)**2 - lsv**2 + min_dist**2) 

145 L = sorted([abs(i) for i in d])[0] / 2. + eps 

146 if L > maxl: 

147 maxl = L 

148 tmpf.translate(e * maxl) 

149 tmpm.translate(-e * maxl) 

150 

151 # Put the two parts together 

152 for atom in chain(tmpf, tmpm): 

153 indi.append(atom) 

154 

155 parent_message = ':Parents {} {}'.format(f.info['confid'], 

156 m.info['confid']) 

157 return (self.finalize_individual(indi), 

158 self.descriptor + parent_message) 

159 

160 def get_numbers(self, atoms): 

161 """Returns the atomic numbers of the atoms object using only 

162 the elements defined in self.elements""" 

163 ac = atoms.copy() 

164 if self.elements is not None: 

165 del ac[[a.index for a in ac 

166 if a.symbol in self.elements]] 

167 return ac.numbers 

168 

169 def get_vectors_below_min_dist(self, atoms): 

170 """Generator function that returns each vector (between atoms) 

171 that is shorter than the minimum distance for those atom types 

172 (set during the initialization in blmin).""" 

173 norm = np.linalg.norm 

174 ap = atoms.get_positions() 

175 an = atoms.numbers 

176 for i in range(len(atoms)): 

177 pos = atoms[i].position 

178 for j, d in enumerate(norm(k - pos) for k in ap[i:]): 

179 if d == 0: 

180 continue 

181 min_dist = self.blmin[tuple(sorted((an[i], an[j + i])))] 

182 if d < min_dist: 

183 yield atoms[i].position - atoms[j + i].position, min_dist 

184 

185 def get_shortest_dist_vector(self, atoms): 

186 norm = np.linalg.norm 

187 mind = 10000. 

188 ap = atoms.get_positions() 

189 for i in range(len(atoms)): 

190 pos = atoms[i].position 

191 for j, d in enumerate(norm(k - pos) for k in ap[i:]): 

192 if d == 0: 

193 continue 

194 if d < mind: 

195 mind = d 

196 lowpair = (i, j + i) 

197 return atoms[lowpair[0]].position - atoms[lowpair[1]].position