Coverage for /builds/ase/ase/ase/ga/element_mutations.py: 75.84%

269 statements  

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

1# fmt: off 

2 

3"""Mutation classes, that mutate the elements in the supplied 

4atoms objects.""" 

5import numpy as np 

6 

7from ase.data import atomic_numbers 

8from ase.ga.offspring_creator import OffspringCreator 

9 

10 

11def chunks(line, n): 

12 """split a list into smaller chunks""" 

13 return [line[i:i + n] for i in range(0, len(line), n)] 

14 

15 

16class ElementMutation(OffspringCreator): 

17 """The base class for all operators where the elements 

18 of the atoms objects are mutated""" 

19 

20 def __init__(self, element_pool, max_diff_elements, 

21 min_percentage_elements, verbose, num_muts=1, rng=np.random): 

22 OffspringCreator.__init__(self, verbose, num_muts=num_muts, rng=rng) 

23 if not isinstance(element_pool[0], (list, np.ndarray)): 

24 self.element_pools = [element_pool] 

25 else: 

26 self.element_pools = element_pool 

27 

28 if max_diff_elements is None: 

29 self.max_diff_elements = [1e6 for _ in self.element_pools] 

30 elif isinstance(max_diff_elements, int): 

31 self.max_diff_elements = [max_diff_elements] 

32 else: 

33 self.max_diff_elements = max_diff_elements 

34 assert len(self.max_diff_elements) == len(self.element_pools) 

35 

36 if min_percentage_elements is None: 

37 self.min_percentage_elements = [0 for _ in self.element_pools] 

38 elif isinstance(min_percentage_elements, (int, float)): 

39 self.min_percentage_elements = [min_percentage_elements] 

40 else: 

41 self.min_percentage_elements = min_percentage_elements 

42 assert len(self.min_percentage_elements) == len(self.element_pools) 

43 

44 self.min_inputs = 1 

45 

46 def get_new_individual(self, parents): 

47 raise NotImplementedError 

48 

49 def get_mutation_index_list_and_choices(self, atoms): 

50 """Returns a list of the indices that are going to 

51 be mutated and a list of possible elements to mutate 

52 to. The lists obey the criteria set in the initialization. 

53 """ 

54 itbm_ok = False 

55 while not itbm_ok: 

56 itbm = self.rng.choice(range(len(atoms))) # index to be mutated 

57 itbm_ok = True 

58 for i, e in enumerate(self.element_pools): 

59 if atoms[itbm].symbol in e: 

60 elems = e[:] 

61 elems_in, indices_in = zip(*[(a.symbol, a.index) 

62 for a in atoms 

63 if a.symbol in elems]) 

64 max_diff_elem = self.max_diff_elements[i] 

65 min_percent_elem = self.min_percentage_elements[i] 

66 if min_percent_elem == 0: 

67 min_percent_elem = 1. / len(elems_in) 

68 break 

69 else: 

70 itbm_ok = False 

71 

72 # Check that itbm obeys min/max criteria 

73 diff_elems_in = len(set(elems_in)) 

74 if diff_elems_in == max_diff_elem: 

75 # No more different elements allowed -> one element mutation 

76 ltbm = [] # list to be mutated 

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

78 if atoms[i].symbol == atoms[itbm].symbol: 

79 ltbm.append(i) 

80 else: 

81 # Fewer or too many different elements already 

82 if self.verbose: 

83 print(int(min_percent_elem * len(elems_in)), 

84 min_percent_elem, len(elems_in)) 

85 all_chunks = chunks(indices_in, 

86 int(min_percent_elem * len(elems_in))) 

87 itbm_num_of_elems = 0 

88 for a in atoms: 

89 if a.index == itbm: 

90 break 

91 if a.symbol in elems: 

92 itbm_num_of_elems += 1 

93 ltbm = all_chunks[itbm_num_of_elems // 

94 (int(min_percent_elem * len(elems_in))) - 1] 

95 

96 elems.remove(atoms[itbm].symbol) 

97 

98 return ltbm, elems 

99 

100 

101class RandomElementMutation(ElementMutation): 

102 """Mutation that exchanges an element with a randomly chosen element from 

103 the supplied pool of elements 

104 If the individual consists of different groups of elements the element 

105 pool can be supplied as a list of lists 

106 

107 Parameters: 

108 

109 element_pool: List of elements in the phase space. The elements can be 

110 grouped if the individual consist of different types of elements. 

111 The list should then be a list of lists e.g. [[list1], [list2]] 

112 

113 max_diff_elements: The maximum number of different elements in the 

114 individual. Default is infinite. If the elements are grouped 

115 max_diff_elements should be supplied as a list with each input 

116 corresponding to the elements specified in the same input in 

117 element_pool. 

118 

119 min_percentage_elements: The minimum percentage of any element in the 

120 individual. Default is any number is allowed. If the elements are 

121 grouped min_percentage_elements should be supplied as a list with 

122 each input corresponding to the elements specified in the same input 

123 in element_pool. 

124 

125 rng: Random number generator 

126 By default numpy.random. 

127 

128 Example: element_pool=[[A,B,C,D],[x,y,z]], max_diff_elements=[3,2], 

129 min_percentage_elements=[.25, .5] 

130 An individual could be "D,B,B,C,x,x,x,x,z,z,z,z" 

131 """ 

132 

133 def __init__(self, element_pool, max_diff_elements=None, 

134 min_percentage_elements=None, verbose=False, 

135 num_muts=1, rng=np.random): 

136 ElementMutation.__init__(self, element_pool, max_diff_elements, 

137 min_percentage_elements, verbose, 

138 num_muts=num_muts, rng=rng) 

139 self.descriptor = 'RandomElementMutation' 

140 

141 def get_new_individual(self, parents): 

142 f = parents[0] 

143 

144 indi = self.initialize_individual(f) 

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

146 

147 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

148 

149 new_element = self.rng.choice(choices) 

150 for a in f: 

151 if a.index in ltbm: 

152 a.symbol = new_element 

153 indi.append(a) 

154 

155 return (self.finalize_individual(indi), 

156 self.descriptor + ': Parent {}'.format(f.info['confid'])) 

157 

158 

159def mendeleiev_table(): 

160 r""" 

161 Returns the mendeleiev table as a python list of lists. 

162 Each cell contains either None or a pair (symbol, atomic number), 

163 or a list of pairs for the cells \* and \**. 

164 """ 

165 import re 

166 elems = 'HHeLiBeBCNOFNeNaMgAlSiPSClArKCaScTiVCrMnFeCoNiCuZnGaGeAsSeBrKrRb' 

167 elems += 'SrYZrNbMoTcRuRhPdAgCdInSnSbTeIXeCsBaLaCePrNdPmSmEuGdTbDyHoErTm' 

168 elems += 'YbLuHfTaWReOsIrPtAuHgTlPbBiPoAtRnFrRaAcThPaUNpPuAmCmBkCfEsFmMd' 

169 elems += 'NoLrRfDbSgBhHsMtDsRgUubUutUuqUupUuhUusUuo' 

170 L = [(e, i + 1) 

171 for (i, e) in enumerate(re.compile('[A-Z][a-z]*').findall(elems))] 

172 for i, j in ((88, 103), (56, 71)): 

173 L[i] = L[i:j] 

174 L[i + 1:] = L[j:] 

175 for i, j in ((12, 10), (4, 10), (1, 16)): 

176 L[i:i] = [None] * j 

177 return [L[18 * i:18 * (i + 1)] for i in range(7)] 

178 

179 

180def get_row_column(element): 

181 """Returns the row and column of the element in the periodic table. 

182 Note that Lanthanides and Actinides are defined to be group (column) 

183 3 elements""" 

184 t = mendeleiev_table() 

185 en = (element, atomic_numbers[element]) 

186 for i in range(len(t)): 

187 for j in range(len(t[i])): 

188 if en == t[i][j]: 

189 return i, j 

190 elif isinstance(t[i][j], list): 

191 # Lanthanide or Actinide 

192 if en in t[i][j]: 

193 return i, 3 

194 

195 

196def get_periodic_table_distance(e1, e2): 

197 rc1 = np.array(get_row_column(e1)) 

198 rc2 = np.array(get_row_column(e2)) 

199 return sum(np.abs(rc1 - rc2)) 

200 

201 

202class MoveDownMutation(ElementMutation): 

203 """ 

204 Mutation that exchanges an element with an element one step 

205 (or more steps if fewer is forbidden) down the same 

206 column in the periodic table. 

207 

208 This mutation is introduced and used in: 

209 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014) 

210 

211 The idea behind is that elements close to each other in the 

212 periodic table is chemically similar, and therefore exhibit 

213 similar properties. An individual in the population is 

214 typically close to fittest possible, exchanging an element 

215 with a similar element will normally result in a slight 

216 increase (or decrease) in fitness. 

217 

218 Parameters: 

219 

220 element_pool: List of elements in the phase space. The elements can be 

221 grouped if the individual consist of different types of elements. 

222 The list should then be a list of lists e.g. [[list1], [list2]] 

223 

224 max_diff_elements: The maximum number of different elements in the 

225 individual. Default is infinite. If the elements are grouped 

226 max_diff_elements should be supplied as a list with each input 

227 corresponding to the elements specified in the same input in 

228 element_pool. 

229 

230 min_percentage_elements: The minimum percentage of any element in the 

231 individual. Default is any number is allowed. If the elements are 

232 grouped min_percentage_elements should be supplied as a list with 

233 each input corresponding to the elements specified in the same input 

234 in element_pool. 

235 

236 rng: Random number generator 

237 By default numpy.random. 

238 

239 Example: element_pool=[[A,B,C,D],[x,y,z]], max_diff_elements=[3,2], 

240 min_percentage_elements=[.25, .5] 

241 An individual could be "D,B,B,C,x,x,x,x,z,z,z,z" 

242 """ 

243 

244 def __init__(self, element_pool, max_diff_elements=None, 

245 min_percentage_elements=None, verbose=False, 

246 num_muts=1, rng=np.random): 

247 ElementMutation.__init__(self, element_pool, max_diff_elements, 

248 min_percentage_elements, verbose, 

249 num_muts=num_muts, rng=rng) 

250 self.descriptor = 'MoveDownMutation' 

251 

252 def get_new_individual(self, parents): 

253 f = parents[0] 

254 

255 indi = self.initialize_individual(f) 

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

257 

258 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

259 # periodic table row, periodic table column 

260 ptrow, ptcol = get_row_column(f[ltbm[0]].symbol) 

261 

262 popped = [] 

263 m = 0 

264 for j in range(len(choices)): 

265 e = choices[j - m] 

266 row, column = get_row_column(e) 

267 if row <= ptrow or column != ptcol: 

268 # Throw away if above (lower numbered row) 

269 # or in a different column in the periodic table 

270 popped.append(choices.pop(j - m)) 

271 m += 1 

272 

273 used_descriptor = self.descriptor 

274 if len(choices) == 0: 

275 msg = '{0},{2} cannot be mutated by {1}, ' 

276 msg = msg.format(f.info['confid'], 

277 self.descriptor, 

278 f[ltbm[0]].symbol) 

279 msg += 'doing random mutation instead' 

280 if self.verbose: 

281 print(msg) 

282 used_descriptor = 'RandomElementMutation_from_{0}' 

283 used_descriptor = used_descriptor.format(self.descriptor) 

284 self.rng.shuffle(popped) 

285 choices = popped 

286 else: 

287 # Sorting the element that lie below and in the same column 

288 # in the periodic table so that the one closest below is first 

289 choices.sort(key=lambda x: get_row_column(x)[0]) 

290 new_element = choices[0] 

291 

292 for a in f: 

293 if a.index in ltbm: 

294 a.symbol = new_element 

295 indi.append(a) 

296 

297 return (self.finalize_individual(indi), 

298 used_descriptor + ': Parent {}'.format(f.info['confid'])) 

299 

300 

301class MoveUpMutation(ElementMutation): 

302 """ 

303 Mutation that exchanges an element with an element one step 

304 (or more steps if fewer is forbidden) up the same 

305 column in the periodic table. 

306 

307 This mutation is introduced and used in: 

308 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014) 

309 

310 See MoveDownMutation for the idea behind 

311 

312 Parameters: 

313 

314 element_pool: List of elements in the phase space. The elements can be 

315 grouped if the individual consist of different types of elements. 

316 The list should then be a list of lists e.g. [[list1], [list2]] 

317 

318 max_diff_elements: The maximum number of different elements in the 

319 individual. Default is infinite. If the elements are grouped 

320 max_diff_elements should be supplied as a list with each input 

321 corresponding to the elements specified in the same input in 

322 element_pool. 

323 

324 min_percentage_elements: The minimum percentage of any element in the 

325 individual. Default is any number is allowed. If the elements are 

326 grouped min_percentage_elements should be supplied as a list with 

327 each input corresponding to the elements specified in the same input 

328 in element_pool. 

329 

330 rng: Random number generator 

331 By default numpy.random. 

332 

333 Example: element_pool=[[A,B,C,D],[x,y,z]], max_diff_elements=[3,2], 

334 min_percentage_elements=[.25, .5] 

335 An individual could be "D,B,B,C,x,x,x,x,z,z,z,z" 

336 """ 

337 

338 def __init__(self, element_pool, max_diff_elements=None, 

339 min_percentage_elements=None, verbose=False, num_muts=1, 

340 rng=np.random): 

341 ElementMutation.__init__(self, element_pool, max_diff_elements, 

342 min_percentage_elements, verbose, 

343 num_muts=num_muts, rng=rng) 

344 self.descriptor = 'MoveUpMutation' 

345 

346 def get_new_individual(self, parents): 

347 f = parents[0] 

348 

349 indi = self.initialize_individual(f) 

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

351 

352 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

353 

354 # periodic table row, periodic table column 

355 ptrow, ptcol = get_row_column(f[ltbm[0]].symbol) 

356 

357 popped = [] 

358 m = 0 

359 for j in range(len(choices)): 

360 e = choices[j - m] 

361 row, column = get_row_column(e) 

362 if row >= ptrow or column != ptcol: 

363 # Throw away if below (higher numbered row) 

364 # or in a different column in the periodic table 

365 popped.append(choices.pop(j - m)) 

366 m += 1 

367 

368 used_descriptor = self.descriptor 

369 if len(choices) == 0: 

370 msg = '{0},{2} cannot be mutated by {1}, ' 

371 msg = msg.format(f.info['confid'], 

372 self.descriptor, 

373 f[ltbm[0]].symbol) 

374 msg += 'doing random mutation instead' 

375 if self.verbose: 

376 print(msg) 

377 used_descriptor = 'RandomElementMutation_from_{0}' 

378 used_descriptor = used_descriptor.format(self.descriptor) 

379 self.rng.shuffle(popped) 

380 choices = popped 

381 else: 

382 # Sorting the element that lie above and in the same column 

383 # in the periodic table so that the one closest above is first 

384 choices.sort(key=lambda x: get_row_column(x)[0], reverse=True) 

385 new_element = choices[0] 

386 

387 for a in f: 

388 if a.index in ltbm: 

389 a.symbol = new_element 

390 indi.append(a) 

391 

392 return (self.finalize_individual(indi), 

393 used_descriptor + ': Parent {}'.format(f.info['confid'])) 

394 

395 

396class MoveRightMutation(ElementMutation): 

397 """ 

398 Mutation that exchanges an element with an element one step 

399 (or more steps if fewer is forbidden) to the right in the 

400 same row in the periodic table. 

401 

402 This mutation is introduced and used in: 

403 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014) 

404 

405 See MoveDownMutation for the idea behind 

406 

407 Parameters: 

408 

409 element_pool: List of elements in the phase space. The elements can be 

410 grouped if the individual consist of different types of elements. 

411 The list should then be a list of lists e.g. [[list1], [list2]] 

412 

413 max_diff_elements: The maximum number of different elements in the 

414 individual. Default is infinite. If the elements are grouped 

415 max_diff_elements should be supplied as a list with each input 

416 corresponding to the elements specified in the same input in 

417 element_pool. 

418 

419 min_percentage_elements: The minimum percentage of any element in the 

420 individual. Default is any number is allowed. If the elements are 

421 grouped min_percentage_elements should be supplied as a list with 

422 each input corresponding to the elements specified in the same input 

423 in element_pool. 

424 

425 rng: Random number generator 

426 By default numpy.random. 

427 

428 Example: element_pool=[[A,B,C,D],[x,y,z]], max_diff_elements=[3,2], 

429 min_percentage_elements=[.25, .5] 

430 An individual could be "D,B,B,C,x,x,x,x,z,z,z,z" 

431 """ 

432 

433 def __init__(self, element_pool, max_diff_elements=None, 

434 min_percentage_elements=None, verbose=False, num_muts=1, 

435 rng=np.random): 

436 ElementMutation.__init__(self, element_pool, max_diff_elements, 

437 min_percentage_elements, verbose, 

438 num_muts=num_muts, rng=rng) 

439 self.descriptor = 'MoveRightMutation' 

440 

441 def get_new_individual(self, parents): 

442 f = parents[0] 

443 

444 indi = self.initialize_individual(f) 

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

446 

447 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

448 # periodic table row, periodic table column 

449 ptrow, ptcol = get_row_column(f[ltbm[0]].symbol) 

450 

451 popped = [] 

452 m = 0 

453 for j in range(len(choices)): 

454 e = choices[j - m] 

455 row, column = get_row_column(e) 

456 if row != ptrow or column <= ptcol: 

457 # Throw away if to the left (a lower numbered column) 

458 # or in a different row in the periodic table 

459 popped.append(choices.pop(j - m)) 

460 m += 1 

461 

462 used_descriptor = self.descriptor 

463 if len(choices) == 0: 

464 msg = '{0},{2} cannot be mutated by {1}, ' 

465 msg = msg.format(f.info['confid'], 

466 self.descriptor, 

467 f[ltbm[0]].symbol) 

468 msg += 'doing random mutation instead' 

469 if self.verbose: 

470 print(msg) 

471 used_descriptor = 'RandomElementMutation_from_{0}' 

472 used_descriptor = used_descriptor.format(self.descriptor) 

473 self.rng.shuffle(popped) 

474 choices = popped 

475 else: 

476 # Sorting so the element closest to the right is first 

477 choices.sort(key=lambda x: get_row_column(x)[1]) 

478 new_element = choices[0] 

479 

480 for a in f: 

481 if a.index in ltbm: 

482 a.symbol = new_element 

483 indi.append(a) 

484 

485 return (self.finalize_individual(indi), 

486 used_descriptor + ': Parent {}'.format(f.info['confid'])) 

487 

488 

489class MoveLeftMutation(ElementMutation): 

490 """ 

491 Mutation that exchanges an element with an element one step 

492 (or more steps if fewer is forbidden) to the left in the 

493 same row in the periodic table. 

494 

495 This mutation is introduced and used in: 

496 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014) 

497 

498 See MoveDownMutation for the idea behind 

499 

500 Parameters: 

501 

502 element_pool: List of elements in the phase space. The elements can be 

503 grouped if the individual consist of different types of elements. 

504 The list should then be a list of lists e.g. [[list1], [list2]] 

505 

506 max_diff_elements: The maximum number of different elements in the 

507 individual. Default is infinite. If the elements are grouped 

508 max_diff_elements should be supplied as a list with each input 

509 corresponding to the elements specified in the same input in 

510 element_pool. 

511 

512 min_percentage_elements: The minimum percentage of any element in the 

513 individual. Default is any number is allowed. If the elements are 

514 grouped min_percentage_elements should be supplied as a list with 

515 each input corresponding to the elements specified in the same input 

516 in element_pool. 

517 

518 rng: Random number generator 

519 By default numpy.random. 

520 

521 Example: element_pool=[[A,B,C,D],[x,y,z]], max_diff_elements=[3,2], 

522 min_percentage_elements=[.25, .5] 

523 An individual could be "D,B,B,C,x,x,x,x,z,z,z,z" 

524 """ 

525 

526 def __init__(self, element_pool, max_diff_elements=None, 

527 min_percentage_elements=None, verbose=False, num_muts=1, 

528 rng=np.random): 

529 ElementMutation.__init__(self, element_pool, max_diff_elements, 

530 min_percentage_elements, verbose, 

531 num_muts=num_muts, rng=rng) 

532 self.descriptor = 'MoveLeftMutation' 

533 

534 def get_new_individual(self, parents): 

535 f = parents[0] 

536 

537 indi = self.initialize_individual(f) 

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

539 

540 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

541 # periodic table row, periodic table column 

542 ptrow, ptcol = get_row_column(f[ltbm[0]].symbol) 

543 

544 popped = [] 

545 m = 0 

546 for j in range(len(choices)): 

547 e = choices[j - m] 

548 row, column = get_row_column(e) 

549 if row != ptrow or column >= ptcol: 

550 # Throw away if to the right (a higher numbered column) 

551 # or in a different row in the periodic table 

552 popped.append(choices.pop(j - m)) 

553 m += 1 

554 

555 used_descriptor = self.descriptor 

556 if len(choices) == 0: 

557 msg = '{0},{2} cannot be mutated by {1}, ' 

558 msg = msg.format(f.info['confid'], 

559 self.descriptor, 

560 f[ltbm[0]].symbol) 

561 msg += 'doing random mutation instead' 

562 if self.verbose: 

563 print(msg) 

564 used_descriptor = 'RandomElementMutation_from_{0}' 

565 used_descriptor = used_descriptor.format(self.descriptor) 

566 self.rng.shuffle(popped) 

567 choices = popped 

568 else: 

569 # Sorting so the element closest to the left is first 

570 choices.sort(key=lambda x: get_row_column(x)[1], reverse=True) 

571 new_element = choices[0] 

572 

573 for a in f: 

574 if a.index in ltbm: 

575 a.symbol = new_element 

576 indi.append(a) 

577 

578 return (self.finalize_individual(indi), 

579 used_descriptor + ':Parent {}'.format(f.info['confid'])) 

580 

581 

582class FullElementMutation(OffspringCreator): 

583 """Mutation that exchanges an all elements of a certain type with another 

584 randomly chosen element from the supplied pool of elements. Any constraints 

585 on the mutation are inhereted from the original candidate. 

586 

587 Parameters: 

588 

589 element_pool: List of elements in the phase space. The elements can be 

590 grouped if the individual consist of different types of elements. 

591 The list should then be a list of lists e.g. [[list1], [list2]] 

592 

593 rng: Random number generator 

594 By default numpy.random. 

595 """ 

596 

597 def __init__(self, element_pool, verbose=False, num_muts=1, rng=np.random): 

598 OffspringCreator.__init__(self, verbose, num_muts=num_muts, rng=rng) 

599 self.descriptor = 'FullElementMutation' 

600 if not isinstance(element_pool[0], (list, np.ndarray)): 

601 self.element_pools = [element_pool] 

602 else: 

603 self.element_pools = element_pool 

604 

605 def get_new_individual(self, parents): 

606 f = parents[0] 

607 

608 indi = self.initialize_individual(f) 

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

610 

611 # Randomly choose an element to mutate in the current individual. 

612 old_element = self.rng.choice([a.symbol for a in f]) 

613 # Find the list containing the chosen element. By choosing a new 

614 # element from the same list, the percentages are not altered. 

615 for i in range(len(self.element_pools)): 

616 if old_element in self.element_pools[i]: 

617 lm = i 

618 

619 not_val = True 

620 while not_val: 

621 new_element = self.rng.choice(self.element_pools[lm]) 

622 not_val = new_element == old_element 

623 

624 for a in f: 

625 if a.symbol == old_element: 

626 a.symbol = new_element 

627 indi.append(a) 

628 

629 return (self.finalize_individual(indi), 

630 self.descriptor + ': Parent {}'.format(f.info['confid']))