Coverage for /builds/ase/ase/ase/ga/standardmutations.py: 74.06%

374 statements  

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

1# fmt: off 

2 

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

4from math import cos, pi, sin 

5 

6import numpy as np 

7 

8from ase import Atoms 

9from ase.calculators.lammps.coordinatetransform import calc_rotated_cell 

10from ase.cell import Cell 

11from ase.ga.offspring_creator import CombinationMutation, OffspringCreator 

12from ase.ga.utilities import ( 

13 atoms_too_close, 

14 atoms_too_close_two_sets, 

15 gather_atoms_by_tag, 

16 get_rotation_matrix, 

17) 

18 

19 

20class RattleMutation(OffspringCreator): 

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

22 

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

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

25 

26 Parameters: 

27 

28 blmin: Dictionary defining the minimum distance between atoms 

29 after the rattle. 

30 

31 n_top: Number of atoms optimized by the GA. 

32 

33 rattle_strength: Strength with which the atoms are moved. 

34 

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

36 

37 test_dist_to_slab: whether to also make sure that the distances 

38 between the atoms and the slab satisfy the blmin. 

39 

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

41 molecular identity. Same-tag atoms will then be 

42 displaced collectively, so that the internal 

43 geometry is preserved. 

44 

45 rng: Random number generator 

46 By default numpy.random. 

47 """ 

48 

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

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

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

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

53 self.blmin = blmin 

54 self.n_top = n_top 

55 self.rattle_strength = rattle_strength 

56 self.rattle_prop = rattle_prop 

57 self.test_dist_to_slab = test_dist_to_slab 

58 self.use_tags = use_tags 

59 

60 self.descriptor = 'RattleMutation' 

61 self.min_inputs = 1 

62 

63 def get_new_individual(self, parents): 

64 f = parents[0] 

65 

66 indi = self.mutate(f) 

67 if indi is None: 

68 return indi, 'mutation: rattle' 

69 

70 indi = self.initialize_individual(f, indi) 

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

72 

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

74 

75 def mutate(self, atoms): 

76 """Does the actual mutation.""" 

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

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

79 atoms = atoms[-N:] 

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

81 pos_ref = atoms.get_positions() 

82 num = atoms.get_atomic_numbers() 

83 cell = atoms.get_cell() 

84 pbc = atoms.get_pbc() 

85 st = 2. * self.rattle_strength 

86 

87 count = 0 

88 maxcount = 1000 

89 too_close = True 

90 while too_close and count < maxcount: 

91 count += 1 

92 pos = pos_ref.copy() 

93 ok = False 

94 for tag in np.unique(tags): 

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

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

97 ok = True 

98 r = self.rng.random(3) 

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

100 

101 if not ok: 

102 # Nothing got rattled 

103 continue 

104 

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

106 too_close = atoms_too_close( 

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

108 if not too_close and self.test_dist_to_slab: 

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

110 

111 if count == maxcount: 

112 return None 

113 

114 mutant = slab + top 

115 return mutant 

116 

117 

118class PermutationMutation(OffspringCreator): 

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

120 

121 Parameters: 

122 

123 n_top: Number of atoms optimized by the GA. 

124 

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

126 

127 test_dist_to_slab: whether to also make sure that the distances 

128 between the atoms and the slab satisfy the blmin. 

129 

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

131 molecular identity. Permutations will then happen 

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

133 positions of two moieties while preserving their 

134 internal geometries. 

135 

136 blmin: Dictionary defining the minimum distance between atoms 

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

138 no such check is performed. 

139 

140 rng: Random number generator 

141 By default numpy.random. 

142 """ 

143 

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

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

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

147 self.n_top = n_top 

148 self.probability = probability 

149 self.test_dist_to_slab = test_dist_to_slab 

150 self.use_tags = use_tags 

151 self.blmin = blmin 

152 

153 self.descriptor = 'PermutationMutation' 

154 self.min_inputs = 1 

155 

156 def get_new_individual(self, parents): 

157 f = parents[0] 

158 

159 indi = self.mutate(f) 

160 if indi is None: 

161 return indi, 'mutation: permutation' 

162 

163 indi = self.initialize_individual(f, indi) 

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

165 

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

167 

168 def mutate(self, atoms): 

169 """Does the actual mutation.""" 

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

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

172 atoms = atoms[-N:] 

173 if self.use_tags: 

174 gather_atoms_by_tag(atoms) 

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

176 pos_ref = atoms.get_positions() 

177 num = atoms.get_atomic_numbers() 

178 cell = atoms.get_cell() 

179 pbc = atoms.get_pbc() 

180 symbols = atoms.get_chemical_symbols() 

181 

182 unique_tags = np.unique(tags) 

183 n = len(unique_tags) 

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

185 

186 sym = [] 

187 for tag in unique_tags: 

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

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

190 sym.append(s) 

191 

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

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

194 

195 count = 0 

196 maxcount = 1000 

197 too_close = True 

198 while too_close and count < maxcount: 

199 count += 1 

200 pos = pos_ref.copy() 

201 for _ in range(swaps): 

202 i = j = 0 

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

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

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

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

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

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

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

210 pos[ind1] += cop2 - cop1 

211 pos[ind2] += cop1 - cop2 

212 

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

214 if self.blmin is None: 

215 too_close = False 

216 else: 

217 too_close = atoms_too_close( 

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

219 if not too_close and self.test_dist_to_slab: 

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

221 

222 if count == maxcount: 

223 return None 

224 

225 mutant = slab + top 

226 return mutant 

227 

228 

229class MirrorMutation(OffspringCreator): 

230 """A mirror mutation, as described in 

231 TO BE PUBLISHED. 

232 

233 This mutation mirrors half of the cluster in a 

234 randomly oriented cutting plane discarding the other half. 

235 

236 Parameters: 

237 

238 blmin: Dictionary defining the minimum allowed 

239 distance between atoms. 

240 

241 n_top: Number of atoms the GA optimizes. 

242 

243 reflect: Defines if the mirrored half is also reflected 

244 perpendicular to the mirroring plane. 

245 

246 rng: Random number generator 

247 By default numpy.random. 

248 """ 

249 

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

251 verbose=False): 

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

253 self.blmin = blmin 

254 self.n_top = n_top 

255 self.reflect = reflect 

256 

257 self.descriptor = 'MirrorMutation' 

258 self.min_inputs = 1 

259 

260 def get_new_individual(self, parents): 

261 f = parents[0] 

262 

263 indi = self.mutate(f) 

264 if indi is None: 

265 return indi, 'mutation: mirror' 

266 

267 indi = self.initialize_individual(f, indi) 

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

269 

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

271 

272 def mutate(self, atoms): 

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

274 reflect = self.reflect 

275 tc = True 

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

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

278 num = top.numbers 

279 unique_types = list(set(num)) 

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

281 n_tries = 1000 

282 counter = 0 

283 changed = False 

284 

285 while tc and counter < n_tries: 

286 counter += 1 

287 cand = top.copy() 

288 pos = cand.get_positions() 

289 

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

291 

292 # first select a randomly oriented cutting plane 

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

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

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

296 n = np.array(n) 

297 

298 # Calculate all atoms signed distance to the cutting plane 

299 D = [] 

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

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

302 D.append((i, d)) 

303 

304 # Sort the atoms by their signed distance 

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

306 nu_taken = {} 

307 

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

309 p_use = [] 

310 n_use = [] 

311 for (i, d) in D: 

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

313 nu_taken[num[i]] = 0 

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

315 p_use.append(pos[i]) 

316 n_use.append(num[i]) 

317 nu_taken[num[i]] += 1 

318 

319 # calculate the mirrored position and add these. 

320 pn = [] 

321 for p in p_use: 

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

323 if reflect: 

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

325 pn.append(pt) 

326 

327 n_use.extend(n_use) 

328 p_use.extend(pn) 

329 

330 # In the case of an uneven number of 

331 # atoms we need to add one extra 

332 for n in nu: 

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

334 continue 

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

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

337 if n_use[i] == n: 

338 del p_use[i] 

339 del n_use[i] 

340 break 

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

342 

343 # Make sure we have the correct number of atoms 

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

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

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

347 continue 

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

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

350 tn = n_use[i] 

351 tp = p_use[i] 

352 n_use[i] = n_use[j] 

353 p_use[i] = p_use[j] 

354 p_use[j] = tp 

355 n_use[j] = tn 

356 

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

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

359 

360 tc = atoms_too_close(cand, self.blmin) 

361 if tc: 

362 continue 

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

364 

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

366 reflect = not reflect 

367 changed = True 

368 

369 tot = slab + cand 

370 

371 if counter == n_tries: 

372 return None 

373 return tot 

374 

375 

376class StrainMutation(OffspringCreator): 

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

378 

379 For more information, see also: 

380 

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

382 <10.1016/j.cpc.2006.07.020>` 

383 

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

385 <10.1016/j.cpc.2010.07.048>` 

386 

387 After initialization of the mutation, a scaling volume 

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

389 constraints) is typically generated from the population, 

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

391 GA run. 

392 

393 Parameters: 

394 

395 blmin: dict 

396 The closest allowed interatomic distances on the form: 

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

398 

399 cellbounds: ase.ga.utilities.CellBounds instance 

400 Describes limits on the cell shape, see 

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

402 

403 stddev: float 

404 Standard deviation used in the generation of the 

405 strain matrix elements. 

406 

407 number_of_variable_cell_vectors: int (default 3) 

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

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

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

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

412 case, etc. 

413 

414 use_tags: boolean 

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

416 

417 rng: Random number generator 

418 By default numpy.random. 

419 """ 

420 

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

422 number_of_variable_cell_vectors=3, use_tags=False, 

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

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

425 self.blmin = blmin 

426 self.cellbounds = cellbounds 

427 self.stddev = stddev 

428 self.number_of_variable_cell_vectors = number_of_variable_cell_vectors 

429 self.use_tags = use_tags 

430 

431 self.scaling_volume = None 

432 self.descriptor = 'StrainMutation' 

433 self.min_inputs = 1 

434 

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

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

437 

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

439 

440 n_adapt: number of best candidates in the population that 

441 are used to calculate the new scaling volume 

442 """ 

443 if not n_adapt: 

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

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

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

447 

448 if not self.scaling_volume: 

449 self.scaling_volume = v_new 

450 else: 

451 volumes = [self.scaling_volume, v_new] 

452 weights = [1 - w_adapt, w_adapt] 

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

454 

455 def get_new_individual(self, parents): 

456 f = parents[0] 

457 

458 indi = self.mutate(f) 

459 if indi is None: 

460 return indi, 'mutation: strain' 

461 

462 indi = self.initialize_individual(f, indi) 

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

464 

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

466 

467 def mutate(self, atoms): 

468 """ Does the actual mutation. """ 

469 cell_ref = atoms.get_cell() 

470 pos_ref = atoms.get_positions() 

471 

472 if self.scaling_volume is None: 

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

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

475 vol_ref = atoms.get_volume() 

476 else: 

477 vol_ref = self.scaling_volume 

478 

479 if self.use_tags: 

480 tags = atoms.get_tags() 

481 gather_atoms_by_tag(atoms) 

482 pos = atoms.get_positions() 

483 

484 mutant = atoms.copy() 

485 

486 count = 0 

487 too_close = True 

488 maxcount = 1000 

489 while too_close and count < maxcount: 

490 count += 1 

491 

492 # generating the strain matrix: 

493 strain = np.identity(3) 

494 for i in range(self.number_of_variable_cell_vectors): 

495 for j in range(i + 1): 

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

497 if i == j: 

498 strain[i, j] += r 

499 else: 

500 epsilon = 0.5 * r 

501 strain[i, j] += epsilon 

502 strain[j, i] += epsilon 

503 

504 # applying the strain: 

505 cell_new = np.dot(strain, cell_ref) 

506 

507 # convert the submatrix with the variable cell vectors 

508 # to a lower triangular form 

509 cell_new = calc_rotated_cell(cell_new) 

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

511 cell_new[i] = cell_ref[i] 

512 

513 cell_new = Cell(cell_new) 

514 

515 # volume scaling: 

516 if self.number_of_variable_cell_vectors > 0: 

517 scaling = vol_ref / cell_new.volume 

518 scaling **= 1. / self.number_of_variable_cell_vectors 

519 cell_new[:self.number_of_variable_cell_vectors] *= scaling 

520 

521 # check cell dimensions: 

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

523 continue 

524 

525 # ensure non-variable cell vectors are indeed unchanged 

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

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

528 

529 # check that the volume is correct 

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

531 

532 # apply the new unit cell and scale 

533 # the atomic positions accordingly 

534 mutant.set_cell(cell_ref, scale_atoms=False) 

535 

536 if self.use_tags: 

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

538 for tag in np.unique(tags): 

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

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

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

542 mutant.positions[select] += disp 

543 else: 

544 mutant.set_positions(pos_ref) 

545 

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

547 mutant.wrap() 

548 

549 # check the interatomic distances 

550 too_close = atoms_too_close(mutant, self.blmin, 

551 use_tags=self.use_tags) 

552 

553 if count == maxcount: 

554 mutant = None 

555 

556 return mutant 

557 

558 

559class PermuStrainMutation(CombinationMutation): 

560 """Combination of PermutationMutation and StrainMutation. 

561 

562 For more information, see also: 

563 

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

565 <10.1016/j.cpc.2010.07.048>` 

566 

567 Parameters: 

568 

569 permutationmutation: OffspringCreator instance 

570 A mutation that permutes atom types. 

571 

572 strainmutation: OffspringCreator instance 

573 A mutation that mutates by straining. 

574 """ 

575 

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

577 super().__init__(permutationmutation, 

578 strainmutation, 

579 verbose=verbose) 

580 self.descriptor = 'permustrain' 

581 

582 

583class RotationalMutation(OffspringCreator): 

584 """Mutates a candidate by applying random rotations 

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

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

587 

588 Only performs whole-molecule rotations, no internal 

589 rotations. 

590 

591 For more information, see also: 

592 

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

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

595 

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

597 

598 Parameters: 

599 

600 blmin: dict 

601 The closest allowed interatomic distances on the form: 

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

603 

604 n_top: int or None 

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

606 

607 fraction: float 

608 Fraction of the moieties to be rotated. 

609 

610 tags: None or list of integers 

611 Specifies, respectively, whether all moieties or only those 

612 with matching tags are eligible for rotation. 

613 

614 min_angle: float 

615 Minimal angle (in radians) for each rotation; 

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

617 

618 test_dist_to_slab: boolean 

619 Whether also the distances to the slab 

620 should be checked to satisfy the blmin. 

621 

622 rng: Random number generator 

623 By default numpy.random. 

624 """ 

625 

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

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

628 verbose=False): 

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

630 self.blmin = blmin 

631 self.n_top = n_top 

632 self.fraction = fraction 

633 self.tags = tags 

634 self.min_angle = min_angle 

635 self.test_dist_to_slab = test_dist_to_slab 

636 self.descriptor = 'RotationalMutation' 

637 self.min_inputs = 1 

638 

639 def get_new_individual(self, parents): 

640 f = parents[0] 

641 

642 indi = self.mutate(f) 

643 if indi is None: 

644 return indi, 'mutation: rotational' 

645 

646 indi = self.initialize_individual(f, indi) 

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

648 

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

650 

651 def mutate(self, atoms): 

652 """Does the actual mutation.""" 

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

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

655 atoms = atoms[-N:] 

656 

657 mutant = atoms.copy() 

658 gather_atoms_by_tag(mutant) 

659 pos = mutant.get_positions() 

660 tags = mutant.get_tags() 

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

662 

663 indices = {} 

664 for tag in np.unique(tags): 

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

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

667 indices[tag] = hits 

668 

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

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

671 replace=False) 

672 

673 too_close = True 

674 count = 0 

675 maxcount = 10000 

676 while too_close and count < maxcount: 

677 newpos = np.copy(pos) 

678 for tag in chosen_tags: 

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

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

681 

682 if len(p) == 2: 

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

684 while True: 

685 axis = self.rng.random(3) 

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

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

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

689 break 

690 else: 

691 axis = self.rng.random(3) 

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

693 

694 angle = self.min_angle 

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

696 

697 m = get_rotation_matrix(axis, angle) 

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

699 

700 mutant.set_positions(newpos) 

701 mutant.wrap() 

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

703 count += 1 

704 

705 if not too_close and self.test_dist_to_slab: 

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

707 

708 if count == maxcount: 

709 mutant = None 

710 else: 

711 mutant = slab + mutant 

712 

713 return mutant 

714 

715 

716class RattleRotationalMutation(CombinationMutation): 

717 """Combination of RattleMutation and RotationalMutation. 

718 

719 Parameters: 

720 

721 rattlemutation: OffspringCreator instance 

722 A mutation that rattles atoms. 

723 

724 rotationalmutation: OffspringCreator instance 

725 A mutation that rotates moieties. 

726 """ 

727 

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

729 super().__init__(rattlemutation, 

730 rotationalmutation, 

731 verbose=verbose) 

732 self.descriptor = 'rattlerotational'