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
« 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
5from ase.data import atomic_numbers
6from ase.ga.offspring_creator import OffspringCreator
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)]
14class ElementMutation(OffspringCreator):
15 """The base class for all operators where the elements
16 of the atoms objects are mutated"""
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
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)
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)
42 self.min_inputs = 1
44 def get_new_individual(self, parents):
45 raise NotImplementedError
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
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]
94 elems.remove(atoms[itbm].symbol)
96 return ltbm, elems
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
105 Parameters:
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]]
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.
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.
123 rng: Random number generator
124 By default numpy.random.
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 """
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'
139 def get_new_individual(self, parents):
140 f = parents[0]
142 indi = self.initialize_individual(f)
143 indi.info['data']['parents'] = [f.info['confid']]
145 ltbm, choices = self.get_mutation_index_list_and_choices(f)
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)
153 return (self.finalize_individual(indi),
154 self.descriptor + ': Parent {}'.format(f.info['confid']))
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)]
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
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))
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.
206 This mutation is introduced and used in:
207 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014)
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.
216 Parameters:
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]]
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.
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.
234 rng: Random number generator
235 By default numpy.random.
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 """
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'
250 def get_new_individual(self, parents):
251 f = parents[0]
253 indi = self.initialize_individual(f)
254 indi.info['data']['parents'] = [f.info['confid']]
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)
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
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]
290 for a in f:
291 if a.index in ltbm:
292 a.symbol = new_element
293 indi.append(a)
295 return (self.finalize_individual(indi),
296 used_descriptor + ': Parent {}'.format(f.info['confid']))
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.
305 This mutation is introduced and used in:
306 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014)
308 See MoveDownMutation for the idea behind
310 Parameters:
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]]
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.
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.
328 rng: Random number generator
329 By default numpy.random.
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 """
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'
344 def get_new_individual(self, parents):
345 f = parents[0]
347 indi = self.initialize_individual(f)
348 indi.info['data']['parents'] = [f.info['confid']]
350 ltbm, choices = self.get_mutation_index_list_and_choices(f)
352 # periodic table row, periodic table column
353 ptrow, ptcol = get_row_column(f[ltbm[0]].symbol)
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
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]
385 for a in f:
386 if a.index in ltbm:
387 a.symbol = new_element
388 indi.append(a)
390 return (self.finalize_individual(indi),
391 used_descriptor + ': Parent {}'.format(f.info['confid']))
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.
400 This mutation is introduced and used in:
401 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014)
403 See MoveDownMutation for the idea behind
405 Parameters:
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]]
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.
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.
423 rng: Random number generator
424 By default numpy.random.
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 """
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'
439 def get_new_individual(self, parents):
440 f = parents[0]
442 indi = self.initialize_individual(f)
443 indi.info['data']['parents'] = [f.info['confid']]
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)
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
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]
478 for a in f:
479 if a.index in ltbm:
480 a.symbol = new_element
481 indi.append(a)
483 return (self.finalize_individual(indi),
484 used_descriptor + ': Parent {}'.format(f.info['confid']))
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.
493 This mutation is introduced and used in:
494 P. B. Jensen et al., Phys. Chem. Chem. Phys., 16, 36, 19732-19740 (2014)
496 See MoveDownMutation for the idea behind
498 Parameters:
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]]
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.
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.
516 rng: Random number generator
517 By default numpy.random.
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 """
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'
532 def get_new_individual(self, parents):
533 f = parents[0]
535 indi = self.initialize_individual(f)
536 indi.info['data']['parents'] = [f.info['confid']]
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)
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
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]
571 for a in f:
572 if a.index in ltbm:
573 a.symbol = new_element
574 indi.append(a)
576 return (self.finalize_individual(indi),
577 used_descriptor + ':Parent {}'.format(f.info['confid']))
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.
585 Parameters:
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]]
591 rng: Random number generator
592 By default numpy.random.
593 """
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
603 def get_new_individual(self, parents):
604 f = parents[0]
606 indi = self.initialize_individual(f)
607 indi.info['data']['parents'] = [f.info['confid']]
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
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
622 for a in f:
623 if a.symbol == old_element:
624 a.symbol = new_element
625 indi.append(a)
627 return (self.finalize_individual(indi),
628 self.descriptor + ': Parent {}'.format(f.info['confid']))