Coverage for /builds/debichem-team/python-ase/ase/ga/standardmutations.py: 74.06%

374 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1"""A collection of mutations that can be used.""" 

2from math import cos, pi, sin 

3 

4import numpy as np 

5 

6from ase import Atoms 

7from ase.calculators.lammps.coordinatetransform import calc_rotated_cell 

8from ase.cell import Cell 

9from ase.ga.offspring_creator import CombinationMutation, OffspringCreator 

10from ase.ga.utilities import ( 

11 atoms_too_close, 

12 atoms_too_close_two_sets, 

13 gather_atoms_by_tag, 

14 get_rotation_matrix, 

15) 

16 

17 

18class RattleMutation(OffspringCreator): 

19 """An implementation of the rattle mutation as described in: 

20 

21 R.L. Johnston Dalton Transactions, Vol. 22, 

22 No. 22. (2003), pp. 4193-4207 

23 

24 Parameters: 

25 

26 blmin: Dictionary defining the minimum distance between atoms 

27 after the rattle. 

28 

29 n_top: Number of atoms optimized by the GA. 

30 

31 rattle_strength: Strength with which the atoms are moved. 

32 

33 rattle_prop: The probability with which each atom is rattled. 

34 

35 test_dist_to_slab: whether to also make sure that the distances 

36 between the atoms and the slab satisfy the blmin. 

37 

38 use_tags: if True, the atomic tags will be used to preserve 

39 molecular identity. Same-tag atoms will then be 

40 displaced collectively, so that the internal 

41 geometry is preserved. 

42 

43 rng: Random number generator 

44 By default numpy.random. 

45 """ 

46 

47 def __init__(self, blmin, n_top, rattle_strength=0.8, 

48 rattle_prop=0.4, test_dist_to_slab=True, use_tags=False, 

49 verbose=False, rng=np.random): 

50 OffspringCreator.__init__(self, verbose, rng=rng) 

51 self.blmin = blmin 

52 self.n_top = n_top 

53 self.rattle_strength = rattle_strength 

54 self.rattle_prop = rattle_prop 

55 self.test_dist_to_slab = test_dist_to_slab 

56 self.use_tags = use_tags 

57 

58 self.descriptor = 'RattleMutation' 

59 self.min_inputs = 1 

60 

61 def get_new_individual(self, parents): 

62 f = parents[0] 

63 

64 indi = self.mutate(f) 

65 if indi is None: 

66 return indi, 'mutation: rattle' 

67 

68 indi = self.initialize_individual(f, indi) 

69 indi.info['data']['parents'] = [f.info['confid']] 

70 

71 return self.finalize_individual(indi), 'mutation: rattle' 

72 

73 def mutate(self, atoms): 

74 """Does the actual mutation.""" 

75 N = len(atoms) if self.n_top is None else self.n_top 

76 slab = atoms[:len(atoms) - N] 

77 atoms = atoms[-N:] 

78 tags = atoms.get_tags() if self.use_tags else np.arange(N) 

79 pos_ref = atoms.get_positions() 

80 num = atoms.get_atomic_numbers() 

81 cell = atoms.get_cell() 

82 pbc = atoms.get_pbc() 

83 st = 2. * self.rattle_strength 

84 

85 count = 0 

86 maxcount = 1000 

87 too_close = True 

88 while too_close and count < maxcount: 

89 count += 1 

90 pos = pos_ref.copy() 

91 ok = False 

92 for tag in np.unique(tags): 

93 select = np.where(tags == tag) 

94 if self.rng.random() < self.rattle_prop: 

95 ok = True 

96 r = self.rng.random(3) 

97 pos[select] += st * (r - 0.5) 

98 

99 if not ok: 

100 # Nothing got rattled 

101 continue 

102 

103 top = Atoms(num, positions=pos, cell=cell, pbc=pbc, tags=tags) 

104 too_close = atoms_too_close( 

105 top, self.blmin, use_tags=self.use_tags) 

106 if not too_close and self.test_dist_to_slab: 

107 too_close = atoms_too_close_two_sets(top, slab, self.blmin) 

108 

109 if count == maxcount: 

110 return None 

111 

112 mutant = slab + top 

113 return mutant 

114 

115 

116class PermutationMutation(OffspringCreator): 

117 """Mutation that permutes a percentage of the atom types in the cluster. 

118 

119 Parameters: 

120 

121 n_top: Number of atoms optimized by the GA. 

122 

123 probability: The probability with which an atom is permuted. 

124 

125 test_dist_to_slab: whether to also make sure that the distances 

126 between the atoms and the slab satisfy the blmin. 

127 

128 use_tags: if True, the atomic tags will be used to preserve 

129 molecular identity. Permutations will then happen 

130 at the molecular level, i.e. swapping the center-of- 

131 positions of two moieties while preserving their 

132 internal geometries. 

133 

134 blmin: Dictionary defining the minimum distance between atoms 

135 after the permutation. If equal to None (the default), 

136 no such check is performed. 

137 

138 rng: Random number generator 

139 By default numpy.random. 

140 """ 

141 

142 def __init__(self, n_top, probability=0.33, test_dist_to_slab=True, 

143 use_tags=False, blmin=None, rng=np.random, verbose=False): 

144 OffspringCreator.__init__(self, verbose, rng=rng) 

145 self.n_top = n_top 

146 self.probability = probability 

147 self.test_dist_to_slab = test_dist_to_slab 

148 self.use_tags = use_tags 

149 self.blmin = blmin 

150 

151 self.descriptor = 'PermutationMutation' 

152 self.min_inputs = 1 

153 

154 def get_new_individual(self, parents): 

155 f = parents[0] 

156 

157 indi = self.mutate(f) 

158 if indi is None: 

159 return indi, 'mutation: permutation' 

160 

161 indi = self.initialize_individual(f, indi) 

162 indi.info['data']['parents'] = [f.info['confid']] 

163 

164 return self.finalize_individual(indi), 'mutation: permutation' 

165 

166 def mutate(self, atoms): 

167 """Does the actual mutation.""" 

168 N = len(atoms) if self.n_top is None else self.n_top 

169 slab = atoms[:len(atoms) - N] 

170 atoms = atoms[-N:] 

171 if self.use_tags: 

172 gather_atoms_by_tag(atoms) 

173 tags = atoms.get_tags() if self.use_tags else np.arange(N) 

174 pos_ref = atoms.get_positions() 

175 num = atoms.get_atomic_numbers() 

176 cell = atoms.get_cell() 

177 pbc = atoms.get_pbc() 

178 symbols = atoms.get_chemical_symbols() 

179 

180 unique_tags = np.unique(tags) 

181 n = len(unique_tags) 

182 swaps = int(np.ceil(n * self.probability / 2.)) 

183 

184 sym = [] 

185 for tag in unique_tags: 

186 indices = np.where(tags == tag)[0] 

187 s = ''.join([symbols[j] for j in indices]) 

188 sym.append(s) 

189 

190 assert len(np.unique(sym)) > 1, \ 

191 'Permutations with one atom (or molecule) type is not valid' 

192 

193 count = 0 

194 maxcount = 1000 

195 too_close = True 

196 while too_close and count < maxcount: 

197 count += 1 

198 pos = pos_ref.copy() 

199 for _ in range(swaps): 

200 i = j = 0 

201 while sym[i] == sym[j]: 

202 i = self.rng.randint(0, high=n) 

203 j = self.rng.randint(0, high=n) 

204 ind1 = np.where(tags == i) 

205 ind2 = np.where(tags == j) 

206 cop1 = np.mean(pos[ind1], axis=0) 

207 cop2 = np.mean(pos[ind2], axis=0) 

208 pos[ind1] += cop2 - cop1 

209 pos[ind2] += cop1 - cop2 

210 

211 top = Atoms(num, positions=pos, cell=cell, pbc=pbc, tags=tags) 

212 if self.blmin is None: 

213 too_close = False 

214 else: 

215 too_close = atoms_too_close( 

216 top, self.blmin, use_tags=self.use_tags) 

217 if not too_close and self.test_dist_to_slab: 

218 too_close = atoms_too_close_two_sets(top, slab, self.blmin) 

219 

220 if count == maxcount: 

221 return None 

222 

223 mutant = slab + top 

224 return mutant 

225 

226 

227class MirrorMutation(OffspringCreator): 

228 """A mirror mutation, as described in 

229 TO BE PUBLISHED. 

230 

231 This mutation mirrors half of the cluster in a 

232 randomly oriented cutting plane discarding the other half. 

233 

234 Parameters: 

235 

236 blmin: Dictionary defining the minimum allowed 

237 distance between atoms. 

238 

239 n_top: Number of atoms the GA optimizes. 

240 

241 reflect: Defines if the mirrored half is also reflected 

242 perpendicular to the mirroring plane. 

243 

244 rng: Random number generator 

245 By default numpy.random. 

246 """ 

247 

248 def __init__(self, blmin, n_top, reflect=False, rng=np.random, 

249 verbose=False): 

250 OffspringCreator.__init__(self, verbose, rng=rng) 

251 self.blmin = blmin 

252 self.n_top = n_top 

253 self.reflect = reflect 

254 

255 self.descriptor = 'MirrorMutation' 

256 self.min_inputs = 1 

257 

258 def get_new_individual(self, parents): 

259 f = parents[0] 

260 

261 indi = self.mutate(f) 

262 if indi is None: 

263 return indi, 'mutation: mirror' 

264 

265 indi = self.initialize_individual(f, indi) 

266 indi.info['data']['parents'] = [f.info['confid']] 

267 

268 return self.finalize_individual(indi), 'mutation: mirror' 

269 

270 def mutate(self, atoms): 

271 """ Do the mutation of the atoms input. """ 

272 reflect = self.reflect 

273 tc = True 

274 slab = atoms[0:len(atoms) - self.n_top] 

275 top = atoms[len(atoms) - self.n_top: len(atoms)] 

276 num = top.numbers 

277 unique_types = list(set(num)) 

278 nu = {u: sum(num == u) for u in unique_types} 

279 n_tries = 1000 

280 counter = 0 

281 changed = False 

282 

283 while tc and counter < n_tries: 

284 counter += 1 

285 cand = top.copy() 

286 pos = cand.get_positions() 

287 

288 cm = np.average(top.get_positions(), axis=0) 

289 

290 # first select a randomly oriented cutting plane 

291 theta = pi * self.rng.random() 

292 phi = 2. * pi * self.rng.random() 

293 n = (cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta)) 

294 n = np.array(n) 

295 

296 # Calculate all atoms signed distance to the cutting plane 

297 D = [] 

298 for (i, p) in enumerate(pos): 

299 d = np.dot(p - cm, n) 

300 D.append((i, d)) 

301 

302 # Sort the atoms by their signed distance 

303 D.sort(key=lambda x: x[1]) 

304 nu_taken = {} 

305 

306 # Select half of the atoms needed for a full cluster 

307 p_use = [] 

308 n_use = [] 

309 for (i, d) in D: 

310 if num[i] not in nu_taken.keys(): 

311 nu_taken[num[i]] = 0 

312 if nu_taken[num[i]] < nu[num[i]] / 2.: 

313 p_use.append(pos[i]) 

314 n_use.append(num[i]) 

315 nu_taken[num[i]] += 1 

316 

317 # calculate the mirrored position and add these. 

318 pn = [] 

319 for p in p_use: 

320 pt = p - 2. * np.dot(p - cm, n) * n 

321 if reflect: 

322 pt = -pt + 2 * cm + 2 * n * np.dot(pt - cm, n) 

323 pn.append(pt) 

324 

325 n_use.extend(n_use) 

326 p_use.extend(pn) 

327 

328 # In the case of an uneven number of 

329 # atoms we need to add one extra 

330 for n in nu: 

331 if nu[n] % 2 == 0: 

332 continue 

333 while sum(n_use == n) > nu[n]: 

334 for i in range(int(len(n_use) / 2), len(n_use)): 

335 if n_use[i] == n: 

336 del p_use[i] 

337 del n_use[i] 

338 break 

339 assert sum(n_use == n) == nu[n] 

340 

341 # Make sure we have the correct number of atoms 

342 # and rearrange the atoms so they are in the right order 

343 for i in range(len(n_use)): 

344 if num[i] == n_use[i]: 

345 continue 

346 for j in range(i + 1, len(n_use)): 

347 if n_use[j] == num[i]: 

348 tn = n_use[i] 

349 tp = p_use[i] 

350 n_use[i] = n_use[j] 

351 p_use[i] = p_use[j] 

352 p_use[j] = tp 

353 n_use[j] = tn 

354 

355 # Finally we check that nothing is too close in the end product. 

356 cand = Atoms(num, p_use, cell=slab.get_cell(), pbc=slab.get_pbc()) 

357 

358 tc = atoms_too_close(cand, self.blmin) 

359 if tc: 

360 continue 

361 tc = atoms_too_close_two_sets(slab, cand, self.blmin) 

362 

363 if not changed and counter > n_tries // 2: 

364 reflect = not reflect 

365 changed = True 

366 

367 tot = slab + cand 

368 

369 if counter == n_tries: 

370 return None 

371 return tot 

372 

373 

374class StrainMutation(OffspringCreator): 

375 """ Mutates a candidate by applying a randomly generated strain. 

376 

377 For more information, see also: 

378 

379 * :doi:`Glass, Oganov, Hansen, Comp. Phys. Comm. 175 (2006) 713-720 

380 <10.1016/j.cpc.2006.07.020>` 

381 

382 * :doi:`Lonie, Zurek, Comp. Phys. Comm. 182 (2011) 372-387 

383 <10.1016/j.cpc.2010.07.048>` 

384 

385 After initialization of the mutation, a scaling volume 

386 (to which each mutated structure is scaled before checking the 

387 constraints) is typically generated from the population, 

388 which is then also occasionally updated in the course of the 

389 GA run. 

390 

391 Parameters: 

392 

393 blmin: dict 

394 The closest allowed interatomic distances on the form: 

395 {(Z, Z*): dist, ...}, where Z and Z* are atomic numbers. 

396 

397 cellbounds: ase.ga.utilities.CellBounds instance 

398 Describes limits on the cell shape, see 

399 :class:`~ase.ga.utilities.CellBounds`. 

400 

401 stddev: float 

402 Standard deviation used in the generation of the 

403 strain matrix elements. 

404 

405 number_of_variable_cell_vectors: int (default 3) 

406 The number of variable cell vectors (1, 2 or 3). 

407 To keep things simple, it is the 'first' vectors which 

408 will be treated as variable, i.e. the 'a' vector in the 

409 univariate case, the 'a' and 'b' vectors in the bivariate 

410 case, etc. 

411 

412 use_tags: boolean 

413 Whether to use the atomic tags to preserve molecular identity. 

414 

415 rng: Random number generator 

416 By default numpy.random. 

417 """ 

418 

419 def __init__(self, blmin, cellbounds=None, stddev=0.7, 

420 number_of_variable_cell_vectors=3, use_tags=False, 

421 rng=np.random, verbose=False): 

422 OffspringCreator.__init__(self, verbose, rng=rng) 

423 self.blmin = blmin 

424 self.cellbounds = cellbounds 

425 self.stddev = stddev 

426 self.number_of_variable_cell_vectors = number_of_variable_cell_vectors 

427 self.use_tags = use_tags 

428 

429 self.scaling_volume = None 

430 self.descriptor = 'StrainMutation' 

431 self.min_inputs = 1 

432 

433 def update_scaling_volume(self, population, w_adapt=0.5, n_adapt=0): 

434 """Function to initialize or update the scaling volume in a GA run. 

435 

436 w_adapt: weight of the new vs the old scaling volume 

437 

438 n_adapt: number of best candidates in the population that 

439 are used to calculate the new scaling volume 

440 """ 

441 if not n_adapt: 

442 # if not set, take best 20% of the population 

443 n_adapt = int(np.ceil(0.2 * len(population))) 

444 v_new = np.mean([a.get_volume() for a in population[:n_adapt]]) 

445 

446 if not self.scaling_volume: 

447 self.scaling_volume = v_new 

448 else: 

449 volumes = [self.scaling_volume, v_new] 

450 weights = [1 - w_adapt, w_adapt] 

451 self.scaling_volume = np.average(volumes, weights=weights) 

452 

453 def get_new_individual(self, parents): 

454 f = parents[0] 

455 

456 indi = self.mutate(f) 

457 if indi is None: 

458 return indi, 'mutation: strain' 

459 

460 indi = self.initialize_individual(f, indi) 

461 indi.info['data']['parents'] = [f.info['confid']] 

462 

463 return self.finalize_individual(indi), 'mutation: strain' 

464 

465 def mutate(self, atoms): 

466 """ Does the actual mutation. """ 

467 cell_ref = atoms.get_cell() 

468 pos_ref = atoms.get_positions() 

469 

470 if self.scaling_volume is None: 

471 # The scaling_volume has not been set (yet), 

472 # so we give it the same volume as the parent 

473 vol_ref = atoms.get_volume() 

474 else: 

475 vol_ref = self.scaling_volume 

476 

477 if self.use_tags: 

478 tags = atoms.get_tags() 

479 gather_atoms_by_tag(atoms) 

480 pos = atoms.get_positions() 

481 

482 mutant = atoms.copy() 

483 

484 count = 0 

485 too_close = True 

486 maxcount = 1000 

487 while too_close and count < maxcount: 

488 count += 1 

489 

490 # generating the strain matrix: 

491 strain = np.identity(3) 

492 for i in range(self.number_of_variable_cell_vectors): 

493 for j in range(i + 1): 

494 r = self.rng.normal(loc=0., scale=self.stddev) 

495 if i == j: 

496 strain[i, j] += r 

497 else: 

498 epsilon = 0.5 * r 

499 strain[i, j] += epsilon 

500 strain[j, i] += epsilon 

501 

502 # applying the strain: 

503 cell_new = np.dot(strain, cell_ref) 

504 

505 # convert the submatrix with the variable cell vectors 

506 # to a lower triangular form 

507 cell_new = calc_rotated_cell(cell_new) 

508 for i in range(self.number_of_variable_cell_vectors, 3): 

509 cell_new[i] = cell_ref[i] 

510 

511 cell_new = Cell(cell_new) 

512 

513 # volume scaling: 

514 if self.number_of_variable_cell_vectors > 0: 

515 scaling = vol_ref / cell_new.volume 

516 scaling **= 1. / self.number_of_variable_cell_vectors 

517 cell_new[:self.number_of_variable_cell_vectors] *= scaling 

518 

519 # check cell dimensions: 

520 if not self.cellbounds.is_within_bounds(cell_new): 

521 continue 

522 

523 # ensure non-variable cell vectors are indeed unchanged 

524 for i in range(self.number_of_variable_cell_vectors, 3): 

525 assert np.allclose(cell_new[i], cell_ref[i]) 

526 

527 # check that the volume is correct 

528 assert np.isclose(vol_ref, cell_new.volume) 

529 

530 # apply the new unit cell and scale 

531 # the atomic positions accordingly 

532 mutant.set_cell(cell_ref, scale_atoms=False) 

533 

534 if self.use_tags: 

535 transfo = np.linalg.solve(cell_ref, cell_new) 

536 for tag in np.unique(tags): 

537 select = np.where(tags == tag) 

538 cop = np.mean(pos[select], axis=0) 

539 disp = np.dot(cop, transfo) - cop 

540 mutant.positions[select] += disp 

541 else: 

542 mutant.set_positions(pos_ref) 

543 

544 mutant.set_cell(cell_new, scale_atoms=not self.use_tags) 

545 mutant.wrap() 

546 

547 # check the interatomic distances 

548 too_close = atoms_too_close(mutant, self.blmin, 

549 use_tags=self.use_tags) 

550 

551 if count == maxcount: 

552 mutant = None 

553 

554 return mutant 

555 

556 

557class PermuStrainMutation(CombinationMutation): 

558 """Combination of PermutationMutation and StrainMutation. 

559 

560 For more information, see also: 

561 

562 * :doi:`Lonie, Zurek, Comp. Phys. Comm. 182 (2011) 372-387 

563 <10.1016/j.cpc.2010.07.048>` 

564 

565 Parameters: 

566 

567 permutationmutation: OffspringCreator instance 

568 A mutation that permutes atom types. 

569 

570 strainmutation: OffspringCreator instance 

571 A mutation that mutates by straining. 

572 """ 

573 

574 def __init__(self, permutationmutation, strainmutation, verbose=False): 

575 super().__init__(permutationmutation, 

576 strainmutation, 

577 verbose=verbose) 

578 self.descriptor = 'permustrain' 

579 

580 

581class RotationalMutation(OffspringCreator): 

582 """Mutates a candidate by applying random rotations 

583 to multi-atom moieties in the structure (atoms with 

584 the same tag are considered part of one such moiety). 

585 

586 Only performs whole-molecule rotations, no internal 

587 rotations. 

588 

589 For more information, see also: 

590 

591 * `Zhu Q., Oganov A.R., Glass C.W., Stokes H.T, 

592 Acta Cryst. (2012), B68, 215-226.`__ 

593 

594 __ https://dx.doi.org/10.1107/S0108768112017466 

595 

596 Parameters: 

597 

598 blmin: dict 

599 The closest allowed interatomic distances on the form: 

600 {(Z, Z*): dist, ...}, where Z and Z* are atomic numbers. 

601 

602 n_top: int or None 

603 The number of atoms to optimize (None = include all). 

604 

605 fraction: float 

606 Fraction of the moieties to be rotated. 

607 

608 tags: None or list of integers 

609 Specifies, respectively, whether all moieties or only those 

610 with matching tags are eligible for rotation. 

611 

612 min_angle: float 

613 Minimal angle (in radians) for each rotation; 

614 should lie in the interval [0, pi]. 

615 

616 test_dist_to_slab: boolean 

617 Whether also the distances to the slab 

618 should be checked to satisfy the blmin. 

619 

620 rng: Random number generator 

621 By default numpy.random. 

622 """ 

623 

624 def __init__(self, blmin, n_top=None, fraction=0.33, tags=None, 

625 min_angle=1.57, test_dist_to_slab=True, rng=np.random, 

626 verbose=False): 

627 OffspringCreator.__init__(self, verbose, rng=rng) 

628 self.blmin = blmin 

629 self.n_top = n_top 

630 self.fraction = fraction 

631 self.tags = tags 

632 self.min_angle = min_angle 

633 self.test_dist_to_slab = test_dist_to_slab 

634 self.descriptor = 'RotationalMutation' 

635 self.min_inputs = 1 

636 

637 def get_new_individual(self, parents): 

638 f = parents[0] 

639 

640 indi = self.mutate(f) 

641 if indi is None: 

642 return indi, 'mutation: rotational' 

643 

644 indi = self.initialize_individual(f, indi) 

645 indi.info['data']['parents'] = [f.info['confid']] 

646 

647 return self.finalize_individual(indi), 'mutation: rotational' 

648 

649 def mutate(self, atoms): 

650 """Does the actual mutation.""" 

651 N = len(atoms) if self.n_top is None else self.n_top 

652 slab = atoms[:len(atoms) - N] 

653 atoms = atoms[-N:] 

654 

655 mutant = atoms.copy() 

656 gather_atoms_by_tag(mutant) 

657 pos = mutant.get_positions() 

658 tags = mutant.get_tags() 

659 eligible_tags = tags if self.tags is None else self.tags 

660 

661 indices = {} 

662 for tag in np.unique(tags): 

663 hits = np.where(tags == tag)[0] 

664 if len(hits) > 1 and tag in eligible_tags: 

665 indices[tag] = hits 

666 

667 n_rot = int(np.ceil(len(indices) * self.fraction)) 

668 chosen_tags = self.rng.choice(list(indices.keys()), size=n_rot, 

669 replace=False) 

670 

671 too_close = True 

672 count = 0 

673 maxcount = 10000 

674 while too_close and count < maxcount: 

675 newpos = np.copy(pos) 

676 for tag in chosen_tags: 

677 p = np.copy(newpos[indices[tag]]) 

678 cop = np.mean(p, axis=0) 

679 

680 if len(p) == 2: 

681 line = (p[1] - p[0]) / np.linalg.norm(p[1] - p[0]) 

682 while True: 

683 axis = self.rng.random(3) 

684 axis /= np.linalg.norm(axis) 

685 a = np.arccos(np.dot(axis, line)) 

686 if np.pi / 4 < a < np.pi * 3 / 4: 

687 break 

688 else: 

689 axis = self.rng.random(3) 

690 axis /= np.linalg.norm(axis) 

691 

692 angle = self.min_angle 

693 angle += 2 * (np.pi - self.min_angle) * self.rng.random() 

694 

695 m = get_rotation_matrix(axis, angle) 

696 newpos[indices[tag]] = np.dot(m, (p - cop).T).T + cop 

697 

698 mutant.set_positions(newpos) 

699 mutant.wrap() 

700 too_close = atoms_too_close(mutant, self.blmin, use_tags=True) 

701 count += 1 

702 

703 if not too_close and self.test_dist_to_slab: 

704 too_close = atoms_too_close_two_sets(slab, mutant, self.blmin) 

705 

706 if count == maxcount: 

707 mutant = None 

708 else: 

709 mutant = slab + mutant 

710 

711 return mutant 

712 

713 

714class RattleRotationalMutation(CombinationMutation): 

715 """Combination of RattleMutation and RotationalMutation. 

716 

717 Parameters: 

718 

719 rattlemutation: OffspringCreator instance 

720 A mutation that rattles atoms. 

721 

722 rotationalmutation: OffspringCreator instance 

723 A mutation that rotates moieties. 

724 """ 

725 

726 def __init__(self, rattlemutation, rotationalmutation, verbose=False): 

727 super().__init__(rattlemutation, 

728 rotationalmutation, 

729 verbose=verbose) 

730 self.descriptor = 'rattlerotational'