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
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3"""Mutation classes, that mutate the elements in the supplied
4atoms objects."""
5import numpy as np
7from ase.data import atomic_numbers
8from ase.ga.offspring_creator import OffspringCreator
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)]
16class ElementMutation(OffspringCreator):
17 """The base class for all operators where the elements
18 of the atoms objects are mutated"""
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
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)
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)
44 self.min_inputs = 1
46 def get_new_individual(self, parents):
47 raise NotImplementedError
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
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]
96 elems.remove(atoms[itbm].symbol)
98 return ltbm, elems
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
107 Parameters:
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]]
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.
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.
125 rng: Random number generator
126 By default numpy.random.
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 """
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'
141 def get_new_individual(self, parents):
142 f = parents[0]
144 indi = self.initialize_individual(f)
145 indi.info['data']['parents'] = [f.info['confid']]
147 ltbm, choices = self.get_mutation_index_list_and_choices(f)
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)
155 return (self.finalize_individual(indi),
156 self.descriptor + ': Parent {}'.format(f.info['confid']))
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)]
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
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))
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.
208 This mutation is introduced and used in:
209 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014)
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.
218 Parameters:
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]]
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.
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.
236 rng: Random number generator
237 By default numpy.random.
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 """
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'
252 def get_new_individual(self, parents):
253 f = parents[0]
255 indi = self.initialize_individual(f)
256 indi.info['data']['parents'] = [f.info['confid']]
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)
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
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]
292 for a in f:
293 if a.index in ltbm:
294 a.symbol = new_element
295 indi.append(a)
297 return (self.finalize_individual(indi),
298 used_descriptor + ': Parent {}'.format(f.info['confid']))
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.
307 This mutation is introduced and used in:
308 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014)
310 See MoveDownMutation for the idea behind
312 Parameters:
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]]
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.
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.
330 rng: Random number generator
331 By default numpy.random.
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 """
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'
346 def get_new_individual(self, parents):
347 f = parents[0]
349 indi = self.initialize_individual(f)
350 indi.info['data']['parents'] = [f.info['confid']]
352 ltbm, choices = self.get_mutation_index_list_and_choices(f)
354 # periodic table row, periodic table column
355 ptrow, ptcol = get_row_column(f[ltbm[0]].symbol)
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
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]
387 for a in f:
388 if a.index in ltbm:
389 a.symbol = new_element
390 indi.append(a)
392 return (self.finalize_individual(indi),
393 used_descriptor + ': Parent {}'.format(f.info['confid']))
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.
402 This mutation is introduced and used in:
403 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014)
405 See MoveDownMutation for the idea behind
407 Parameters:
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]]
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.
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.
425 rng: Random number generator
426 By default numpy.random.
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 """
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'
441 def get_new_individual(self, parents):
442 f = parents[0]
444 indi = self.initialize_individual(f)
445 indi.info['data']['parents'] = [f.info['confid']]
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)
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
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]
480 for a in f:
481 if a.index in ltbm:
482 a.symbol = new_element
483 indi.append(a)
485 return (self.finalize_individual(indi),
486 used_descriptor + ': Parent {}'.format(f.info['confid']))
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.
495 This mutation is introduced and used in:
496 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014)
498 See MoveDownMutation for the idea behind
500 Parameters:
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]]
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.
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.
518 rng: Random number generator
519 By default numpy.random.
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 """
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'
534 def get_new_individual(self, parents):
535 f = parents[0]
537 indi = self.initialize_individual(f)
538 indi.info['data']['parents'] = [f.info['confid']]
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)
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
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]
573 for a in f:
574 if a.index in ltbm:
575 a.symbol = new_element
576 indi.append(a)
578 return (self.finalize_individual(indi),
579 used_descriptor + ':Parent {}'.format(f.info['confid']))
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.
587 Parameters:
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]]
593 rng: Random number generator
594 By default numpy.random.
595 """
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
605 def get_new_individual(self, parents):
606 f = parents[0]
608 indi = self.initialize_individual(f)
609 indi.info['data']['parents'] = [f.info['confid']]
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
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
624 for a in f:
625 if a.symbol == old_element:
626 a.symbol = new_element
627 indi.append(a)
629 return (self.finalize_individual(indi),
630 self.descriptor + ': Parent {}'.format(f.info['confid']))