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

269 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

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

2atoms objects.""" 

3import numpy as np 

4 

5from ase.data import atomic_numbers 

6from ase.ga.offspring_creator import OffspringCreator 

7 

8 

9def chunks(line, n): 

10 """split a list into smaller chunks""" 

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

12 

13 

14class ElementMutation(OffspringCreator): 

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

16 of the atoms objects are mutated""" 

17 

18 def __init__(self, element_pool, max_diff_elements, 

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

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

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

22 self.element_pools = [element_pool] 

23 else: 

24 self.element_pools = element_pool 

25 

26 if max_diff_elements is None: 

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

28 elif isinstance(max_diff_elements, int): 

29 self.max_diff_elements = [max_diff_elements] 

30 else: 

31 self.max_diff_elements = max_diff_elements 

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

33 

34 if min_percentage_elements is None: 

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

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

37 self.min_percentage_elements = [min_percentage_elements] 

38 else: 

39 self.min_percentage_elements = min_percentage_elements 

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

41 

42 self.min_inputs = 1 

43 

44 def get_new_individual(self, parents): 

45 raise NotImplementedError 

46 

47 def get_mutation_index_list_and_choices(self, atoms): 

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

49 be mutated and a list of possible elements to mutate 

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

51 """ 

52 itbm_ok = False 

53 while not itbm_ok: 

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

55 itbm_ok = True 

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

57 if atoms[itbm].symbol in e: 

58 elems = e[:] 

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

60 for a in atoms 

61 if a.symbol in elems]) 

62 max_diff_elem = self.max_diff_elements[i] 

63 min_percent_elem = self.min_percentage_elements[i] 

64 if min_percent_elem == 0: 

65 min_percent_elem = 1. / len(elems_in) 

66 break 

67 else: 

68 itbm_ok = False 

69 

70 # Check that itbm obeys min/max criteria 

71 diff_elems_in = len(set(elems_in)) 

72 if diff_elems_in == max_diff_elem: 

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

74 ltbm = [] # list to be mutated 

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

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

77 ltbm.append(i) 

78 else: 

79 # Fewer or too many different elements already 

80 if self.verbose: 

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

82 min_percent_elem, len(elems_in)) 

83 all_chunks = chunks(indices_in, 

84 int(min_percent_elem * len(elems_in))) 

85 itbm_num_of_elems = 0 

86 for a in atoms: 

87 if a.index == itbm: 

88 break 

89 if a.symbol in elems: 

90 itbm_num_of_elems += 1 

91 ltbm = all_chunks[itbm_num_of_elems // 

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

93 

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

95 

96 return ltbm, elems 

97 

98 

99class RandomElementMutation(ElementMutation): 

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

101 the supplied pool of elements 

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

103 pool can be supplied as a list of lists 

104 

105 Parameters: 

106 

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

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

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

110 

111 max_diff_elements: The maximum number of different elements in the 

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

113 max_diff_elements should be supplied as a list with each input 

114 corresponding to the elements specified in the same input in 

115 element_pool. 

116 

117 min_percentage_elements: The minimum percentage of any element in the 

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

119 grouped min_percentage_elements should be supplied as a list with 

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

121 in element_pool. 

122 

123 rng: Random number generator 

124 By default numpy.random. 

125 

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

127 min_percentage_elements=[.25, .5] 

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

129 """ 

130 

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

132 min_percentage_elements=None, verbose=False, 

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

134 ElementMutation.__init__(self, element_pool, max_diff_elements, 

135 min_percentage_elements, verbose, 

136 num_muts=num_muts, rng=rng) 

137 self.descriptor = 'RandomElementMutation' 

138 

139 def get_new_individual(self, parents): 

140 f = parents[0] 

141 

142 indi = self.initialize_individual(f) 

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

144 

145 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

146 

147 new_element = self.rng.choice(choices) 

148 for a in f: 

149 if a.index in ltbm: 

150 a.symbol = new_element 

151 indi.append(a) 

152 

153 return (self.finalize_individual(indi), 

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

155 

156 

157def mendeleiev_table(): 

158 r""" 

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

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

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

162 """ 

163 import re 

164 elems = 'HHeLiBeBCNOFNeNaMgAlSiPSClArKCaScTiVCrMnFeCoNiCuZnGaGeAsSeBrKrRb' 

165 elems += 'SrYZrNbMoTcRuRhPdAgCdInSnSbTeIXeCsBaLaCePrNdPmSmEuGdTbDyHoErTm' 

166 elems += 'YbLuHfTaWReOsIrPtAuHgTlPbBiPoAtRnFrRaAcThPaUNpPuAmCmBkCfEsFmMd' 

167 elems += 'NoLrRfDbSgBhHsMtDsRgUubUutUuqUupUuhUusUuo' 

168 L = [(e, i + 1) 

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

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

171 L[i] = L[i:j] 

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

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

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

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

176 

177 

178def get_row_column(element): 

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

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

181 3 elements""" 

182 t = mendeleiev_table() 

183 en = (element, atomic_numbers[element]) 

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

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

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

187 return i, j 

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

189 # Lanthanide or Actinide 

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

191 return i, 3 

192 

193 

194def get_periodic_table_distance(e1, e2): 

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

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

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

198 

199 

200class MoveDownMutation(ElementMutation): 

201 """ 

202 Mutation that exchanges an element with an element one step 

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

204 column in the periodic table. 

205 

206 This mutation is introduced and used in: 

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

208 

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

210 periodic table is chemically similar, and therefore exhibit 

211 similar properties. An individual in the population is 

212 typically close to fittest possible, exchanging an element 

213 with a similar element will normally result in a slight 

214 increase (or decrease) in fitness. 

215 

216 Parameters: 

217 

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

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

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

221 

222 max_diff_elements: The maximum number of different elements in the 

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

224 max_diff_elements should be supplied as a list with each input 

225 corresponding to the elements specified in the same input in 

226 element_pool. 

227 

228 min_percentage_elements: The minimum percentage of any element in the 

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

230 grouped min_percentage_elements should be supplied as a list with 

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

232 in element_pool. 

233 

234 rng: Random number generator 

235 By default numpy.random. 

236 

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

238 min_percentage_elements=[.25, .5] 

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

240 """ 

241 

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

243 min_percentage_elements=None, verbose=False, 

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

245 ElementMutation.__init__(self, element_pool, max_diff_elements, 

246 min_percentage_elements, verbose, 

247 num_muts=num_muts, rng=rng) 

248 self.descriptor = 'MoveDownMutation' 

249 

250 def get_new_individual(self, parents): 

251 f = parents[0] 

252 

253 indi = self.initialize_individual(f) 

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

255 

256 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

257 # periodic table row, periodic table column 

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

259 

260 popped = [] 

261 m = 0 

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

263 e = choices[j - m] 

264 row, column = get_row_column(e) 

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

266 # Throw away if above (lower numbered row) 

267 # or in a different column in the periodic table 

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

269 m += 1 

270 

271 used_descriptor = self.descriptor 

272 if len(choices) == 0: 

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

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

275 self.descriptor, 

276 f[ltbm[0]].symbol) 

277 msg += 'doing random mutation instead' 

278 if self.verbose: 

279 print(msg) 

280 used_descriptor = 'RandomElementMutation_from_{0}' 

281 used_descriptor = used_descriptor.format(self.descriptor) 

282 self.rng.shuffle(popped) 

283 choices = popped 

284 else: 

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

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

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

288 new_element = choices[0] 

289 

290 for a in f: 

291 if a.index in ltbm: 

292 a.symbol = new_element 

293 indi.append(a) 

294 

295 return (self.finalize_individual(indi), 

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

297 

298 

299class MoveUpMutation(ElementMutation): 

300 """ 

301 Mutation that exchanges an element with an element one step 

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

303 column in the periodic table. 

304 

305 This mutation is introduced and used in: 

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

307 

308 See MoveDownMutation for the idea behind 

309 

310 Parameters: 

311 

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

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

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

315 

316 max_diff_elements: The maximum number of different elements in the 

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

318 max_diff_elements should be supplied as a list with each input 

319 corresponding to the elements specified in the same input in 

320 element_pool. 

321 

322 min_percentage_elements: The minimum percentage of any element in the 

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

324 grouped min_percentage_elements should be supplied as a list with 

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

326 in element_pool. 

327 

328 rng: Random number generator 

329 By default numpy.random. 

330 

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

332 min_percentage_elements=[.25, .5] 

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

334 """ 

335 

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

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

338 rng=np.random): 

339 ElementMutation.__init__(self, element_pool, max_diff_elements, 

340 min_percentage_elements, verbose, 

341 num_muts=num_muts, rng=rng) 

342 self.descriptor = 'MoveUpMutation' 

343 

344 def get_new_individual(self, parents): 

345 f = parents[0] 

346 

347 indi = self.initialize_individual(f) 

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

349 

350 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

351 

352 # periodic table row, periodic table column 

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

354 

355 popped = [] 

356 m = 0 

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

358 e = choices[j - m] 

359 row, column = get_row_column(e) 

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

361 # Throw away if below (higher numbered row) 

362 # or in a different column in the periodic table 

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

364 m += 1 

365 

366 used_descriptor = self.descriptor 

367 if len(choices) == 0: 

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

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

370 self.descriptor, 

371 f[ltbm[0]].symbol) 

372 msg += 'doing random mutation instead' 

373 if self.verbose: 

374 print(msg) 

375 used_descriptor = 'RandomElementMutation_from_{0}' 

376 used_descriptor = used_descriptor.format(self.descriptor) 

377 self.rng.shuffle(popped) 

378 choices = popped 

379 else: 

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

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

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

383 new_element = choices[0] 

384 

385 for a in f: 

386 if a.index in ltbm: 

387 a.symbol = new_element 

388 indi.append(a) 

389 

390 return (self.finalize_individual(indi), 

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

392 

393 

394class MoveRightMutation(ElementMutation): 

395 """ 

396 Mutation that exchanges an element with an element one step 

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

398 same row in the periodic table. 

399 

400 This mutation is introduced and used in: 

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

402 

403 See MoveDownMutation for the idea behind 

404 

405 Parameters: 

406 

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

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

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

410 

411 max_diff_elements: The maximum number of different elements in the 

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

413 max_diff_elements should be supplied as a list with each input 

414 corresponding to the elements specified in the same input in 

415 element_pool. 

416 

417 min_percentage_elements: The minimum percentage of any element in the 

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

419 grouped min_percentage_elements should be supplied as a list with 

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

421 in element_pool. 

422 

423 rng: Random number generator 

424 By default numpy.random. 

425 

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

427 min_percentage_elements=[.25, .5] 

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

429 """ 

430 

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

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

433 rng=np.random): 

434 ElementMutation.__init__(self, element_pool, max_diff_elements, 

435 min_percentage_elements, verbose, 

436 num_muts=num_muts, rng=rng) 

437 self.descriptor = 'MoveRightMutation' 

438 

439 def get_new_individual(self, parents): 

440 f = parents[0] 

441 

442 indi = self.initialize_individual(f) 

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

444 

445 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

446 # periodic table row, periodic table column 

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

448 

449 popped = [] 

450 m = 0 

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

452 e = choices[j - m] 

453 row, column = get_row_column(e) 

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

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

456 # or in a different row in the periodic table 

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

458 m += 1 

459 

460 used_descriptor = self.descriptor 

461 if len(choices) == 0: 

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

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

464 self.descriptor, 

465 f[ltbm[0]].symbol) 

466 msg += 'doing random mutation instead' 

467 if self.verbose: 

468 print(msg) 

469 used_descriptor = 'RandomElementMutation_from_{0}' 

470 used_descriptor = used_descriptor.format(self.descriptor) 

471 self.rng.shuffle(popped) 

472 choices = popped 

473 else: 

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

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

476 new_element = choices[0] 

477 

478 for a in f: 

479 if a.index in ltbm: 

480 a.symbol = new_element 

481 indi.append(a) 

482 

483 return (self.finalize_individual(indi), 

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

485 

486 

487class MoveLeftMutation(ElementMutation): 

488 """ 

489 Mutation that exchanges an element with an element one step 

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

491 same row in the periodic table. 

492 

493 This mutation is introduced and used in: 

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

495 

496 See MoveDownMutation for the idea behind 

497 

498 Parameters: 

499 

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

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

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

503 

504 max_diff_elements: The maximum number of different elements in the 

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

506 max_diff_elements should be supplied as a list with each input 

507 corresponding to the elements specified in the same input in 

508 element_pool. 

509 

510 min_percentage_elements: The minimum percentage of any element in the 

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

512 grouped min_percentage_elements should be supplied as a list with 

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

514 in element_pool. 

515 

516 rng: Random number generator 

517 By default numpy.random. 

518 

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

520 min_percentage_elements=[.25, .5] 

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

522 """ 

523 

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

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

526 rng=np.random): 

527 ElementMutation.__init__(self, element_pool, max_diff_elements, 

528 min_percentage_elements, verbose, 

529 num_muts=num_muts, rng=rng) 

530 self.descriptor = 'MoveLeftMutation' 

531 

532 def get_new_individual(self, parents): 

533 f = parents[0] 

534 

535 indi = self.initialize_individual(f) 

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

537 

538 ltbm, choices = self.get_mutation_index_list_and_choices(f) 

539 # periodic table row, periodic table column 

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

541 

542 popped = [] 

543 m = 0 

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

545 e = choices[j - m] 

546 row, column = get_row_column(e) 

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

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

549 # or in a different row in the periodic table 

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

551 m += 1 

552 

553 used_descriptor = self.descriptor 

554 if len(choices) == 0: 

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

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

557 self.descriptor, 

558 f[ltbm[0]].symbol) 

559 msg += 'doing random mutation instead' 

560 if self.verbose: 

561 print(msg) 

562 used_descriptor = 'RandomElementMutation_from_{0}' 

563 used_descriptor = used_descriptor.format(self.descriptor) 

564 self.rng.shuffle(popped) 

565 choices = popped 

566 else: 

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

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

569 new_element = choices[0] 

570 

571 for a in f: 

572 if a.index in ltbm: 

573 a.symbol = new_element 

574 indi.append(a) 

575 

576 return (self.finalize_individual(indi), 

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

578 

579 

580class FullElementMutation(OffspringCreator): 

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

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

583 on the mutation are inhereted from the original candidate. 

584 

585 Parameters: 

586 

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

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

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

590 

591 rng: Random number generator 

592 By default numpy.random. 

593 """ 

594 

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

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

597 self.descriptor = 'FullElementMutation' 

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

599 self.element_pools = [element_pool] 

600 else: 

601 self.element_pools = element_pool 

602 

603 def get_new_individual(self, parents): 

604 f = parents[0] 

605 

606 indi = self.initialize_individual(f) 

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

608 

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

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

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

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

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

614 if old_element in self.element_pools[i]: 

615 lm = i 

616 

617 not_val = True 

618 while not_val: 

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

620 not_val = new_element == old_element 

621 

622 for a in f: 

623 if a.symbol == old_element: 

624 a.symbol = new_element 

625 indi.append(a) 

626 

627 return (self.finalize_individual(indi), 

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