def mutate(self, atoms): """Does the actual mutation.""" N = len(atoms) if self.n_top is None else self.n_top slab = atoms[:len(atoms) - N] atoms = atoms[-N:] if self.use_tags: gather_atoms_by_tag(atoms) tags = atoms.get_tags() if self.use_tags else np.arange(N) pos_ref = atoms.get_positions() num = atoms.get_atomic_numbers() cell = atoms.get_cell() pbc = atoms.get_pbc() symbols = atoms.get_chemical_symbols() unique_tags = np.unique(tags) n = len(unique_tags) swaps = int(np.ceil(n * self.probability / 2.)) sym = [] for tag in unique_tags: indices = np.where(tags == tag)[0] s = ''.join([symbols[j] for j in indices]) sym.append(s) assert len(np.unique(sym)) > 1, \ 'Permutations with one atom (or molecule) type is not valid' count = 0 maxcount = 1000 too_close = True while too_close and count < maxcount: count += 1 pos = pos_ref.copy() for _ in range(swaps): i = j = 0 while sym[i] == sym[j]: i = self.rng.randint(0, high=n) j = self.rng.randint(0, high=n) ind1 = np.where(tags == i) ind2 = np.where(tags == j) cop1 = np.mean(pos[ind1], axis=0) cop2 = np.mean(pos[ind2], axis=0) pos[ind1] += cop2 - cop1 pos[ind2] += cop1 - cop2 top = Atoms(num, positions=pos, cell=cell, pbc=pbc, tags=tags) if self.blmin is None: too_close = False else: too_close = atoms_too_close(top, self.blmin, use_tags=self.use_tags) if not too_close and self.test_dist_to_slab: too_close = atoms_too_close_two_sets(top, slab, self.blmin) if count == maxcount: return None mutant = slab + top return mutant
def mutate(self, atoms): """ Does the actual mutation. """ slab = atoms[0:len(atoms) - self.n_top] pos_ref = atoms.get_positions()[-self.n_top:] num_top = atoms.numbers[-self.n_top:] st = 2. * self.rattle_strength count = 0 tc = True while tc and count < 1000: pos = pos_ref.copy() for i in xrange(len(pos)): if random() < self.rattle_prop: r = np.array([random() for r in xrange(3)]) pos[i] = pos[i] + st * (r - 0.5) top = Atoms(num_top, positions=pos, cell=slab.get_cell(), pbc=slab.get_pbc()) tc = atoms_too_close(top, self.blmin) if not tc: tc = atoms_too_close_two_sets(top, slab, self.blmin) count += 1 if count == 1000: return None tot = slab + top return tot
def mutate(self, atoms): """Does the actual mutation.""" N = len(atoms) if self.n_top is None else self.n_top slab = atoms[:len(atoms) - N] atoms = atoms[-N:] tags = atoms.get_tags() if self.use_tags else np.arange(N) pos_ref = atoms.get_positions() num = atoms.get_atomic_numbers() cell = atoms.get_cell() pbc = atoms.get_pbc() st = 2. * self.rattle_strength count = 0 maxcount = 1000 too_close = True while too_close and count < maxcount: count += 1 pos = pos_ref.copy() ok = False for tag in np.unique(tags): select = np.where(tags == tag) if self.rng.rand() < self.rattle_prop: ok = True r = self.rng.rand(3) pos[select] += st * (r - 0.5) if not ok: # Nothing got rattled continue top = Atoms(num, positions=pos, cell=cell, pbc=pbc, tags=tags) too_close = atoms_too_close(top, self.blmin, use_tags=self.use_tags) if not too_close and self.test_dist_to_slab: too_close = atoms_too_close_two_sets(top, slab, self.blmin) if count == maxcount: return None mutant = slab + top return mutant
def mutate(self, atoms): """ Does the actual mutation. """ slab = atoms[0:len(atoms) - self.n_top] pos_ref = atoms.get_positions()[-self.n_top:] num_top = atoms.numbers[-self.n_top:] st = 2. * self.rattle_strength count = 0 tc = True while tc and count < 1000: pos = pos_ref.copy() for i in xrange(len(pos)): if random() < self.rattle_prop: r = np.array([random() for r in xrange(3)]) pos[i] = pos[i] + st * (r - 0.5) top = Atoms(num_top, positions=pos, cell=slab.get_cell(), pbc=slab.get_pbc()) tc = atoms_too_close(top, self.blmin) if not tc: tc = atoms_too_close_two_sets(top, slab, self.blmin) count += 1 if count == 1000: return None, 'rattle' tot = slab + top return tot
pairing = CutAndSplicePairing(slab, n_top, cd) c3, desc = pairing.get_new_individual([c1, c2]) # verify that the stoichiometry is preserved assert np.all(c3.numbers == c1.numbers) top1 = c1[-n_top:] top2 = c2[-n_top:] top3 = c3[-n_top:] # verify that the positions in the new candidate come from c1 or c2 n1 = -1 * np.ones((n_top,)) n2 = -1 * np.ones((n_top,)) for i in range(n_top): for j in range(n_top): if np.all(top1.positions[j, :] == top3.positions[i, :]): n1[i] = j break elif np.all(top2.positions[j, :] == top3.positions[i, :]): n2[i] = j break assert (n1[i] > -1 and n2[i] == -1) or (n1[i] == -1 and n2[i] > -1) # verify that c3 includes atoms from both c1 and c2 assert len(n1[n1 > -1]) > 0 and len(n2[n2 > -1]) > 0 # verify no atoms too close assert not atoms_too_close(top3, cd)
def mutate(self, atoms): """ Does the actual mutation. """ cell_ref = atoms.get_cell() pos_ref = atoms.get_positions() vol_ref = atoms.get_volume() if self.use_tags: tags = atoms.get_tags() gather_atoms_by_tag(atoms) pos = atoms.get_positions() mutant = atoms.copy() count = 0 too_close = True maxcount = 1000 while too_close and count < maxcount: count += 1 # generating the strain matrix: strain = np.identity(3) for i in range(self.number_of_variable_cell_vectors): for j in range(i + 1): r = self.rng.normal(loc=0., scale=self.stddev) if i == j: strain[i, j] += r else: epsilon = 0.5 * r strain[i, j] += epsilon strain[j, i] += epsilon # applying the strain: cell_new = np.dot(strain, cell_ref) # convert to lower triangular form cell_new = convert_cell(cell_new)[0].T # volume scaling: if self.number_of_variable_cell_vectors > 0: volume = abs(np.linalg.det(cell_new)) if self.scaling_volume is None: # The scaling_volume has not been set (yet), # so we give it the same volume as the parent scaling = vol_ref / volume else: scaling = self.scaling_volume / volume scaling **= 1. / self.number_of_variable_cell_vectors cell_new[:self.number_of_variable_cell_vectors] *= scaling # check cell dimensions: if not self.cellbounds.is_within_bounds(cell_new): continue # ensure non-variable cell vectors are indeed unchanged for i in range(self.number_of_variable_cell_vectors, 3): assert np.allclose(cell_new[i], cell_ref[i]) # apply the new unit cell and scale # the atomic positions accordingly mutant.set_cell(cell_ref, scale_atoms=False) if self.use_tags: transfo = np.linalg.solve(cell_ref, cell_new) for tag in np.unique(tags): select = np.where(tags == tag) cop = np.mean(pos[select], axis=0) disp = np.dot(cop, transfo) - cop mutant.positions[select] += disp else: mutant.set_positions(pos_ref) mutant.set_cell(cell_new, scale_atoms=not self.use_tags) mutant.wrap() # check the interatomic distances too_close = atoms_too_close(mutant, self.blmin, use_tags=self.use_tags) if count == maxcount: mutant = None return mutant
def test_bulk_operators(): h2 = Atoms('H2', positions=[[0, 0, 0], [0, 0, 0.75]]) blocks = [('H', 4), ('H2O', 3), (h2, 2)] # the building blocks volume = 40. * sum([x[1] for x in blocks]) # cell volume in angstrom^3 splits = {(2,): 1, (1,): 1} # cell splitting scheme stoichiometry = [] for block, count in blocks: if type(block) == str: stoichiometry += list(Atoms(block).numbers) * count else: stoichiometry += list(block.numbers) * count atom_numbers = list(set(stoichiometry)) blmin = closest_distances_generator(atom_numbers=atom_numbers, ratio_of_covalent_radii=1.3) cellbounds = CellBounds(bounds={'phi': [30, 150], 'chi': [30, 150], 'psi': [30, 150], 'a': [3, 50], 'b': [3, 50], 'c': [3, 50]}) sg = StartGenerator(blocks, blmin, volume, cellbounds=cellbounds, splits=splits) # Generate 2 candidates a1 = sg.get_new_candidate() a1.info['confid'] = 1 a2 = sg.get_new_candidate() a2.info['confid'] = 2 # Define and test genetic operators pairing = CutAndSplicePairing(blmin, p1=1., p2=0., minfrac=0.15, cellbounds=cellbounds, use_tags=True) a3, desc = pairing.get_new_individual([a1, a2]) cell = a3.get_cell() assert cellbounds.is_within_bounds(cell) assert not atoms_too_close(a3, blmin, use_tags=True) n_top = len(a1) strainmut = StrainMutation(blmin, stddev=0.7, cellbounds=cellbounds, use_tags=True) softmut = SoftMutation(blmin, bounds=[2., 5.], used_modes_file=None, use_tags=True) rotmut = RotationalMutation(blmin, fraction=0.3, min_angle=0.5 * np.pi) rattlemut = RattleMutation(blmin, n_top, rattle_prop=0.3, rattle_strength=0.5, use_tags=True, test_dist_to_slab=False) rattlerotmut = RattleRotationalMutation(rattlemut, rotmut) permut = PermutationMutation(n_top, probability=0.33, test_dist_to_slab=False, use_tags=True, blmin=blmin) combmut = CombinationMutation(rattlemut, rotmut, verbose=True) mutations = [strainmut, softmut, rotmut, rattlemut, rattlerotmut, permut, combmut] for i, mut in enumerate(mutations): a = [a1, a2][i % 2] a3 = None while a3 is None: a3, desc = mut.get_new_individual([a]) cell = a3.get_cell() assert cellbounds.is_within_bounds(cell) assert np.all(a3.numbers == a.numbers) assert not atoms_too_close(a3, blmin, use_tags=True) modes_file = 'modes.txt' softmut_with = SoftMutation(blmin, bounds=[2., 5.], use_tags=True, used_modes_file=modes_file) no_muts = 3 for _ in range(no_muts): softmut_with.get_new_individual([a1]) softmut_with.read_used_modes(modes_file) assert len(list(softmut_with.used_modes.values())[0]) == no_muts os.remove(modes_file) comparator = OFPComparator(recalculate=True) gold = bulk('Au') * (2, 2, 2) assert comparator.looks_like(gold, gold) # This move should not exceed the default threshold gc = gold.copy() gc[0].x += .1 assert comparator.looks_like(gold, gc) # An additional step will exceed the threshold gc[0].x += .2 assert not comparator.looks_like(gold, gc)
def test_film_operators(seed): from ase.ga.startgenerator import StartGenerator from ase.ga.cutandsplicepairing import CutAndSplicePairing from ase.ga.standardmutations import StrainMutation from ase.ga.utilities import (closest_distances_generator, atoms_too_close, CellBounds) import numpy as np from ase import Atoms from ase.build import molecule # set up the random number generator rng = np.random.RandomState(seed) slab = Atoms('', cell=(0, 0, 15), pbc=[True, True, False]) cation, anion = 'Mg', molecule('OH') d_oh = anion.get_distance(0, 1) blocks = [(cation, 4), (anion, 8)] n_top = 4 + 8 * len(anion) use_tags = True num_vcv = 2 box_volume = 8. * n_top blmin = closest_distances_generator(atom_numbers=[1, 8, 12], ratio_of_covalent_radii=0.6) cellbounds = CellBounds( bounds={ 'phi': [0.1 * 180., 0.9 * 180.], 'chi': [0.1 * 180., 0.9 * 180.], 'psi': [0.1 * 180., 0.9 * 180.], 'a': [2, 8], 'b': [2, 8] }) box_to_place_in = [[None, None, 3.], [None, None, [0., 0., 5.]]] sg = StartGenerator(slab, blocks, blmin, box_volume=box_volume, splits={(2, 1): 1}, box_to_place_in=box_to_place_in, number_of_variable_cell_vectors=num_vcv, cellbounds=cellbounds, test_too_far=True, test_dist_to_slab=False, rng=rng) parents = [] for i in range(2): a = None while a is None: a = sg.get_new_candidate() a.info['confid'] = i parents.append(a) assert len(a) == n_top assert len(np.unique(a.get_tags())) == 4 + 8 assert np.allclose(a.get_pbc(), slab.get_pbc()) p = a.get_positions() assert np.min(p[:, 2]) > 3. - 0.5 * d_oh assert np.max(p[:, 2]) < 3. + 5. + 0.5 * d_oh assert not atoms_too_close(a, blmin, use_tags=use_tags) c = a.get_cell() assert np.allclose(c[2], slab.get_cell()[2]) assert cellbounds.is_within_bounds(c) v = a.get_volume() * 5. / 15. assert abs(v - box_volume) < 1e-5 # Test cut-and-splice pairing and strain mutation pairing = CutAndSplicePairing(slab, n_top, blmin, number_of_variable_cell_vectors=num_vcv, p1=1., p2=0., minfrac=0.15, cellbounds=cellbounds, use_tags=use_tags, rng=rng) strainmut = StrainMutation(blmin, cellbounds=cellbounds, number_of_variable_cell_vectors=num_vcv, use_tags=use_tags, rng=rng) strainmut.update_scaling_volume(parents) for operator in [pairing, strainmut]: child = None while child is None: child, desc = operator.get_new_individual(parents) assert not atoms_too_close(child, blmin, use_tags=use_tags) cell = child.get_cell() assert cellbounds.is_within_bounds(cell) assert np.allclose(cell[2], slab.get_cell()[2])
def test_cutandsplicepairing(seed): from ase.ga.startgenerator import StartGenerator from ase.ga.utilities import closest_distances_generator, atoms_too_close from ase.ga.cutandsplicepairing import CutAndSplicePairing import numpy as np from ase.build import fcc111 from ase.constraints import FixAtoms # set up the random number generator rng = np.random.RandomState(seed) # first create two random starting candidates slab = fcc111('Au', size=(4, 4, 2), vacuum=10.0, orthogonal=True) slab.set_constraint(FixAtoms(mask=slab.positions[:, 2] <= 10.)) pos = slab.get_positions() cell = slab.get_cell() p0 = np.array([0., 0., max(pos[:, 2]) + 2.]) v1 = cell[0, :] * 0.8 v2 = cell[1, :] * 0.8 v3 = cell[2, :] v3[2] = 3. blmin = closest_distances_generator(atom_numbers=[47, 79], ratio_of_covalent_radii=0.7) atom_numbers = 2 * [47] + 2 * [79] sg = StartGenerator(slab=slab, blocks=atom_numbers, blmin=blmin, box_to_place_in=[p0, [v1, v2, v3]], rng=rng) c1 = sg.get_new_candidate() c1.info['confid'] = 1 c2 = sg.get_new_candidate() c2.info['confid'] = 2 n_top = len(atom_numbers) pairing = CutAndSplicePairing(slab, n_top, blmin, rng=rng) c3, desc = pairing.get_new_individual([c1, c2]) # verify that the stoichiometry is preserved assert np.all(c3.numbers == c1.numbers) top1 = c1[-n_top:] top2 = c2[-n_top:] top3 = c3[-n_top:] # verify that the positions in the new candidate come from c1 or c2 n1 = -1 * np.ones((n_top, )) n2 = -1 * np.ones((n_top, )) for i in range(n_top): for j in range(n_top): if np.allclose(top1.positions[j, :], top3.positions[i, :], 1e-12): n1[i] = j break elif np.allclose(top2.positions[j, :], top3.positions[i, :], 1e-12): n2[i] = j break assert (n1[i] > -1 and n2[i] == -1) or (n1[i] == -1 and n2[i] > -1) # verify that c3 includes atoms from both c1 and c2 assert len(n1[n1 > -1]) > 0 and len(n2[n2 > -1]) > 0 # verify no atoms too close assert not atoms_too_close(top3, blmin)
def get_new_candidate(self, maxiter=None): """Returns a new candidate. maxiter: upper bound on the total number of times the random position generator is called when generating the new candidate. By default (maxiter=None) no such bound is imposed. If the generator takes too long time to create a new candidate, it may be suitable to specify a finite value. When the bound is exceeded, None is returned. """ pbc = self.slab.get_pbc() # Choose cell splitting r = self.rng.rand() cumprob = 0 for split, prob in self.splits.items(): cumprob += prob if cumprob > r: break # Choose direction(s) along which to split # and by how much directions = [i for i in range(3) if pbc[i]] repeat = [1, 1, 1] if len(directions) > 0: for number in split: d = self.rng.choice(directions) repeat[d] = number repeat = tuple(repeat) # Generate the 'full' unit cell # for the eventual candidates cell = self.generate_unit_cell(repeat) if self.number_of_variable_cell_vectors == 0: assert np.allclose(cell, self.slab.get_cell()) # Make the smaller 'box' in which we are # allowed to place the atoms and which will # then be repeated to fill the 'full' unit cell box = np.copy(cell) for i in range(self.number_of_variable_cell_vectors, 3): box[i] = np.array(self.box_to_place_in[1][i]) box /= np.array([repeat]).T # Here we gather the (reduced) number of blocks # to put in the smaller box, and the 'surplus' # occurring when the block count is not divisible # by the number of repetitions. # E.g. if we have a ('Ti', 4) block and do a # [2, 3, 1] repetition, we employ a ('Ti', 1) # block in the smaller box and delete 2 out 6 # Ti atoms afterwards nrep = int(np.prod(repeat)) blocks, ids, surplus = [], [], [] for i, (block, count) in enumerate(self.blocks): count_part = int(np.ceil(count * 1. / nrep)) blocks.extend([block] * count_part) surplus.append(nrep * count_part - count) ids.extend([i] * count_part) N_blocks = len(blocks) # Shuffle the ordering so different blocks # are added in random order order = np.arange(N_blocks) self.rng.shuffle(order) blocks = [blocks[i] for i in order] ids = np.array(ids)[order] # Add blocks one by one until we have found # a valid candidate blmin = self.blmin blmin_too_far = {key: 2 * val for key, val in blmin.items()} niter = 0 while maxiter is None or niter < maxiter: cand = Atoms('', cell=box, pbc=pbc) for i in range(N_blocks): atoms = blocks[i].copy() atoms.set_tags(i) atoms.set_pbc(pbc) atoms.set_cell(box, scale_atoms=False) while maxiter is None or niter < maxiter: niter += 1 cop = atoms.get_positions().mean(axis=0) pos = np.dot(self.rng.rand(1, 3), box) atoms.translate(pos - cop) if len(atoms) > 1: # Apply a random rotation to multi-atom blocks phi, theta, psi = 360 * self.rng.rand(3) atoms.euler_rotate(phi=phi, theta=0.5 * theta, psi=psi, center=pos) if not atoms_too_close_two_sets(cand, atoms, blmin): cand += atoms break else: # Reached maximum iteration number # Break out of the for loop above cand = None break if cand is None: # Exit the main while loop break # Rebuild the candidate after repeating, # randomly deleting surplus blocks and # sorting back to the original order cand_full = cand.repeat(repeat) tags_full = cand_full.get_tags() for i in range(nrep): tags_full[len(cand) * i:len(cand) * (i + 1)] += i * N_blocks cand_full.set_tags(tags_full) cand = Atoms('', cell=cell, pbc=pbc) ids_full = np.tile(ids, nrep) tag_counter = 0 if len(self.slab) > 0: tag_counter = int(max(self.slab.get_tags())) + 1 for i, (block, count) in enumerate(self.blocks): tags = np.where(ids_full == i)[0] bad = self.rng.choice(tags, size=surplus[i], replace=False) for tag in tags: if tag not in bad: select = [a.index for a in cand_full if a.tag == tag] atoms = cand_full[select] # is indeed a copy! atoms.set_tags(tag_counter) assert len(atoms) == len(block) cand += atoms tag_counter += 1 for i in range(self.number_of_variable_cell_vectors, 3): cand.positions[:, i] += self.box_to_place_in[0][i] # By construction, the minimal interatomic distances # within the structure should already be respected assert not atoms_too_close(cand, blmin, use_tags=True), \ 'This is not supposed to happen; please report this bug' if self.test_dist_to_slab and len(self.slab) > 0: if atoms_too_close_two_sets(self.slab, cand, blmin): continue if self.test_too_far: tags = cand.get_tags() for tag in np.unique(tags): too_far = True indices_i = np.where(tags == tag)[0] indices_j = np.where(tags != tag)[0] too_far = not atoms_too_close_two_sets( cand[indices_i], cand[indices_j], blmin_too_far) if too_far and len(self.slab) > 0: # the block is too far from the rest # but might still be sufficiently # close to the slab too_far = not atoms_too_close_two_sets( cand[indices_i], self.slab, blmin_too_far) if too_far: break else: too_far = False if too_far: continue # Passed all the tests cand = self.slab + cand cand.set_cell(cell, scale_atoms=False) break else: # Reached max iteration count in the while loop return None return cand
def mutate(self, atoms): """ Does the actual mutation. """ cell_ref = atoms.get_cell() pos_ref = atoms.get_positions() vol = atoms.get_volume() if self.use_tags: tags = atoms.get_tags() gather_atoms_by_tag(atoms) pos = atoms.get_positions() mutant = atoms.copy() if self.cellbounds is not None: if not self.cellbounds.is_within_bounds(cell_ref): niggli_reduce(mutant) count = 0 too_close = True maxcount = 1000 while too_close and count < maxcount: mutant.set_cell(cell_ref, scale_atoms=False) mutant.set_positions(pos_ref) # generating the strain matrix: strain = np.identity(3) for i in range(3): for j in range(i + 1): if i == j: strain[i, j] += gauss(0, self.stddev) else: epsilon = 0.5 * gauss(0, self.stddev) strain[i, j] += epsilon strain[j, i] += epsilon # applying the strain: cell_new = np.dot(strain, cell_ref) # volume scaling: v = abs(np.linalg.det(cell_new)) if self.scaling_volume is None: cell_new *= (vol / v)**(1. / 3) else: cell_new *= (self.scaling_volume / v)**(1. / 3) # check cell dimensions: if not self.cellbounds.is_within_bounds(cell_new): continue if self.use_tags: transfo = np.linalg.solve(cell_ref, cell_new) for tag in np.unique(tags): select = np.where(tags == tag) cop = np.mean(pos[select], axis=0) disp = np.dot(cop, transfo) - cop mutant.positions[select] += disp mutant.set_cell(cell_new, scale_atoms=not self.use_tags) # check distances: too_close = atoms_too_close(mutant, self.blmin, use_tags=self.use_tags) count += 1 if count == maxcount: mutant = None return mutant
def mutate(self, atoms): """ Does the actual mutation. """ a = atoms.copy() if inspect.isclass(self.calc): assert issubclass(self.calc, PairwiseHarmonicPotential) calc = self.calc(atoms, rcut=self.rcut) else: calc = self.calc a.set_calculator(calc) if self.use_tags: a = TagFilter(a) pos = a.get_positions() modes = self._calculate_normal_modes(a) # Select the mode along which we want to move the atoms; # The first 3 translational modes as well as previously # applied modes are discarded. keys = np.array(sorted(modes)) index = 3 confid = atoms.info['confid'] if confid in self.used_modes: while index in self.used_modes[confid]: index += 1 self.used_modes[confid].append(index) else: self.used_modes[confid] = [index] if self.used_modes_file is not None: self.write_used_modes(self.used_modes_file) key = keys[index] mode = modes[key].reshape(np.shape(pos)) # Find a suitable amplitude for translation along the mode; # at every trial amplitude both positive and negative # directions are tried. mutant = atoms.copy() amplitude = 0. increment = 0.1 direction = 1 largest_norm = np.max(np.apply_along_axis(np.linalg.norm, 1, mode)) def expand(atoms, positions): if isinstance(atoms, TagFilter): a.set_positions(positions) return a.atoms.get_positions() else: return positions while amplitude * largest_norm < self.bounds[1]: pos_new = pos + direction * amplitude * mode pos_new = expand(a, pos_new) mutant.set_positions(pos_new) mutant.wrap() too_close = atoms_too_close(mutant, self.blmin, use_tags=self.use_tags) if too_close: amplitude -= increment pos_new = pos + direction * amplitude * mode pos_new = expand(a, pos_new) mutant.set_positions(pos_new) mutant.wrap() break if direction == 1: direction = -1 else: direction = 1 amplitude += increment if amplitude * largest_norm < self.bounds[0]: mutant = None return mutant
def mutate(self, atoms): """ Do the mutation of the atoms input. """ reflect = self.reflect tc = True slab = atoms[0:len(atoms) - self.n_top] top = atoms[len(atoms) - self.n_top:len(atoms)] num = top.numbers unique_types = list(set(num)) nu = dict() for u in unique_types: nu[u] = sum(num == u) n_tries = 1000 counter = 0 changed = False while tc and counter < n_tries: counter += 1 cand = top.copy() pos = cand.get_positions() cm = np.average(top.get_positions(), axis=0) # first select a randomly oriented cutting plane theta = pi * random() phi = 2. * pi * random() n = (cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta)) n = np.array(n) # Calculate all atoms signed distance to the cutting plane D = [] for (i, p) in enumerate(pos): d = np.dot(p - cm, n) D.append((i, d)) # Sort the atoms by their signed distance D.sort(key=lambda x: x[1]) nu_taken = dict() # Select half of the atoms needed for a full cluster p_use = [] n_use = [] for (i, d) in D: if num[i] not in nu_taken.keys(): nu_taken[num[i]] = 0 if nu_taken[num[i]] < nu[num[i]] / 2.: p_use.append(pos[i]) n_use.append(num[i]) nu_taken[num[i]] += 1 # calculate the mirrored position and add these. pn = [] for p in p_use: pt = p - 2. * np.dot(p - cm, n) * n if reflect: pt = -pt + 2 * cm + 2 * n * np.dot(pt - cm, n) pn.append(pt) n_use.extend(n_use) p_use.extend(pn) # In the case of an uneven number of # atoms we need to add one extra for n in nu.keys(): if nu[n] % 2 == 0: continue while sum(n_use == n) > nu[n]: for i in xrange(int(len(n_use) / 2), len(n_use)): if n_use[i] == n: del p_use[i] del n_use[i] break assert sum(n_use == n) == nu[n] # Make sure we have the correct number of atoms # and rearrange the atoms so they are in the right order for i in xrange(len(n_use)): if num[i] == n_use[i]: continue for j in xrange(i + 1, len(n_use)): if n_use[j] == num[i]: tn = n_use[i] tp = p_use[i] n_use[i] = n_use[j] p_use[i] = p_use[j] p_use[j] = tp n_use[j] = tn # Finally we check that nothing is too close in the end product. cand = Atoms(num, p_use, cell=slab.get_cell(), pbc=slab.get_pbc()) tc = atoms_too_close(cand, self.blmin) if tc: continue tc = atoms_too_close_two_sets(slab, cand, self.blmin) if not changed and counter > int(n_tries / 2): reflect = not reflect changed = True tot = slab + cand if counter == n_tries: return None return tot
n_top = len(atom_numbers) pairing = CutAndSplicePairing(slab, n_top, cd) c3, desc = pairing.get_new_individual([c1, c2]) # verify that the stoichiometry is preserved assert np.all(c3.numbers == c1.numbers) top1 = c1[-n_top:] top2 = c2[-n_top:] top3 = c3[-n_top:] # verify that the positions in the new candidate come from c1 or c2 n1 = -1 * np.ones((n_top, )) n2 = -1 * np.ones((n_top, )) for i in range(n_top): for j in range(n_top): if np.all(top1.positions[j, :] == top3.positions[i, :]): n1[i] = j break elif np.all(top2.positions[j, :] == top3.positions[i, :]): n2[i] = j break assert (n1[i] > -1 and n2[i] == -1) or (n1[i] == -1 and n2[i] > -1) # verify that c3 includes atoms from both c1 and c2 assert len(n1[n1 > -1]) > 0 and len(n2[n2 > -1]) > 0 # verify no atoms too close assert not atoms_too_close(top3, cd)
def cross(self, a1, a2): """Crosses the two atoms objects and returns one""" if len(a1) != len(self.slab) + self.n_top: raise ValueError('Wrong size of structure to optimize') if len(a1) != len(a2): raise ValueError('The two structures do not have the same length') N = self.n_top # Only consider the atoms to optimize a1 = a1[len(a1) - N:len(a1)] a2 = a2[len(a2) - N:len(a2)] if not np.array_equal(a1.numbers, a2.numbers): err = 'Trying to pair two structures with different stoichiometry' raise ValueError(err) if self.use_tags and not np.array_equal(a1.get_tags(), a2.get_tags()): err = 'Trying to pair two structures with different tags' raise ValueError(err) cell1 = a1.get_cell() cell2 = a2.get_cell() for i in range(self.number_of_variable_cell_vectors, 3): err = 'Unit cells are supposed to be identical in direction %d' assert np.allclose(cell1[i], cell2[i]), (err % i, cell1, cell2) invalid = True counter = 0 maxcount = 1000 a1_copy = a1.copy() a2_copy = a2.copy() # Run until a valid pairing is made or maxcount pairings are tested. while invalid and counter < maxcount: counter += 1 newcell = self.generate_unit_cell(cell1, cell2) if newcell is None: # No valid unit cell could be generated. # This strongly suggests that it is near-impossible # to generate one from these parent cells and it is # better to abort now. break # Choose direction of cutting plane normal if self.number_of_variable_cell_vectors == 0: # Will be generated entirely at random theta = np.pi * self.rng.rand() phi = 2. * np.pi * self.rng.rand() cut_n = np.array([ np.cos(phi) * np.sin(theta), np.sin(phi) * np.sin(theta), np.cos(theta) ]) else: # Pick one of the 'variable' cell vectors cut_n = self.rng.choice(self.number_of_variable_cell_vectors) # Randomly translate parent structures for a_copy, a in zip([a1_copy, a2_copy], [a1, a2]): a_copy.set_positions(a.get_positions()) cell = a_copy.get_cell() for i in range(self.number_of_variable_cell_vectors): r = self.rng.rand() cond1 = i == cut_n and r < self.p1 cond2 = i != cut_n and r < self.p2 if cond1 or cond2: a_copy.positions += self.rng.rand() * cell[i] if self.use_tags: # For correct determination of the center- # of-position of the multi-atom blocks, # we need to group their constituent atoms # together gather_atoms_by_tag(a_copy) else: a_copy.wrap() # Generate the cutting point in scaled coordinates cosp1 = np.average(a1_copy.get_scaled_positions(), axis=0) cosp2 = np.average(a2_copy.get_scaled_positions(), axis=0) cut_p = np.zeros((1, 3)) for i in range(3): if i < self.number_of_variable_cell_vectors: cut_p[0, i] = self.rng.rand() else: cut_p[0, i] = 0.5 * (cosp1[i] + cosp2[i]) # Perform the pairing: child = self._get_pairing(a1_copy, a2_copy, cut_p, cut_n, newcell) if child is None: continue # Verify whether the atoms are too close or not: if atoms_too_close(child, self.blmin, use_tags=self.use_tags): continue if self.test_dist_to_slab and len(self.slab) > 0: if atoms_too_close_two_sets(self.slab, child, self.blmin): continue # Passed all the tests child = self.slab + child child.set_cell(newcell, scale_atoms=False) child.wrap() return child return None
def mutate(self, atoms): """ Do the mutation of the atoms input. """ reflect = self.reflect tc = True slab = atoms[0:len(atoms) - self.n_top] top = atoms[len(atoms) - self.n_top: len(atoms)] num = top.numbers unique_types = list(set(num)) nu = dict() for u in unique_types: nu[u] = sum(num == u) n_tries = 1000 counter = 0 changed = False while tc and counter < n_tries: counter += 1 cand = top.copy() pos = cand.get_positions() cm = np.average(top.get_positions(), axis=0) # first select a randomly oriented cutting plane theta = pi * random() phi = 2. * pi * random() n = (cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta)) n = np.array(n) # Calculate all atoms signed distance to the cutting plane D = [] for (i, p) in enumerate(pos): d = np.dot(p - cm, n) D.append((i, d)) # Sort the atoms by their signed distance D.sort(key=lambda x: x[1]) nu_taken = dict() # Select half of the atoms needed for a full cluster p_use = [] n_use = [] for (i, d) in D: if num[i] not in nu_taken.keys(): nu_taken[num[i]] = 0 if nu_taken[num[i]] < nu[num[i]] / 2.: p_use.append(pos[i]) n_use.append(num[i]) nu_taken[num[i]] += 1 # calculate the mirrored position and add these. pn = [] for p in p_use: pt = p - 2. * np.dot(p - cm, n) * n if reflect: pt = -pt + 2 * cm + 2 * n * np.dot(pt - cm, n) pn.append(pt) n_use.extend(n_use) p_use.extend(pn) # In the case of an uneven number of # atoms we need to add one extra for n in nu.keys(): if nu[n] % 2 == 0: continue while sum(n_use == n) > nu[n]: for i in xrange(int(len(n_use) / 2), len(n_use)): if n_use[i] == n: del p_use[i] del n_use[i] break assert sum(n_use == n) == nu[n] # Make sure we have the correct number of atoms # and rearrange the atoms so they are in the right order for i in xrange(len(n_use)): if num[i] == n_use[i]: continue for j in xrange(i + 1, len(n_use)): if n_use[j] == num[i]: tn = n_use[i] tp = p_use[i] n_use[i] = n_use[j] p_use[i] = p_use[j] p_use[j] = tp n_use[j] = tn # Finally we check that nothing is too close in the end product. cand = Atoms(num, p_use, cell=slab.get_cell(), pbc=slab.get_pbc()) tc = atoms_too_close(cand, self.blmin) if tc: continue tc = atoms_too_close_two_sets(slab, cand, self.blmin) if not changed and counter > int(n_tries / 2): reflect = not reflect changed = True tot = slab + cand if counter == n_tries: return None return tot
def cross(self, a1, a2, test_dist_to_slab=True): """Crosses the two atoms objects and returns one""" if len(a1) != len(self.slab) + self.n_top: raise ValueError('Wrong size of structure to optimize') if len(a1) != len(a2): raise ValueError('The two structures do not have the same length') N = self.n_top # Only consider the atoms to optimize a1 = a1[len(a1) - N:len(a1)] a2 = a2[len(a2) - N:len(a2)] if not np.array_equal(a1.numbers, a2.numbers): err = 'Trying to pair two structures with different stoichiometry' raise ValueError(err) # Find the common center of the two clusters c1cm = np.average(a1.get_positions(), axis=0) c2cm = np.average(a2.get_positions(), axis=0) cutting_point = (c1cm + c2cm) / 2. counter = 0 too_close = True n_max = 1000 # Run until a valid pairing is made or 1000 pairings are tested. while too_close and counter < n_max: # Generate the cutting plane theta = pi * random() phi = 2. * pi * random() n = (cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta)) n = np.array(n) # Get the pairing top = self._get_pairing_(a1, a2, cutting_plane=n, cutting_point=cutting_point) # Check if the candidate is valid too_close = atoms_too_close(top, self.blmin) if not too_close and test_dist_to_slab: too_close = atoms_too_close_two_sets(self.slab, top, self.blmin) # Verify that the generated structure contains atoms from # both parents n1 = -1 * np.ones((N, )) n2 = -1 * np.ones((N, )) for i in xrange(N): for j in xrange(N): if np.all(a1.positions[j, :] == top.positions[i, :]): n1[i] = j break elif np.all(a2.positions[j, :] == top.positions[i, :]): n2[i] = j break assert (n1[i] > -1 and n2[i] == -1) or (n1[i] == -1 and n2[i] > -1) if not (len(n1[n1 > -1]) > 0 and len(n2[n2 > -1]) > 0): too_close = True counter += 1 if counter == n_max: return None return self.slab + top
a1.info['confid'] = 1 a2 = sg.get_new_candidate() a2.info['confid'] = 2 # Define and test genetic operators pairing = CutAndSplicePairing(blmin, p1=1., p2=0., minfrac=0.15, cellbounds=cellbounds, use_tags=True) a3, desc = pairing.get_new_individual([a1, a2]) cell = a3.get_cell() assert cellbounds.is_within_bounds(cell) assert not atoms_too_close(a3, blmin, use_tags=True) n_top = len(a1) strainmut = StrainMutation(blmin, stddev=0.7, cellbounds=cellbounds, use_tags=True) softmut = SoftMutation(blmin, bounds=[2., 5.], used_modes_file=None, use_tags=True) rotmut = RotationalMutation(blmin, fraction=0.3, min_angle=0.5 * np.pi) rattlemut = RattleMutation(blmin, n_top, rattle_prop=0.3, rattle_strength=0.5,
def get_new_candidate(self, maxiter=None): """ Returns a new candidate. maxiter: upper bound on the total number of times the random position generator is called when generating the new candidate. By default (maxiter=None) no such bound is imposed. If the generator takes too long time to create a new candidate, it may be suitable to specify a finite value. When the bound is exceeded, None is returned. """ pbc = [True] * 3 blmin = self.blmin # generating the cell # cell splitting: # choose factors according to the probabilities r = random() cumprob = 0 for split, prob in self.splits.items(): cumprob += prob if cumprob > r: break directions = sample(range(3), len(split)) repeat = [1, 1, 1] for i, d in enumerate(directions): repeat[d] = split[i] repeat = tuple(repeat) nparts = np.product(repeat) target_volume = self.volume / nparts # Randomly create a cell; without loss of generality, # a lower triangular form can be used, with tilt factors # within certain bounds. # For a cell to be valid, the full cell has to satisfy # the cellbounds constraints. Additionally, the length of # each subcell vector has to be greater than the largest # (X,X)-minimal-interatomic-distance in blmin. if self.cell is not None: full_cell = np.copy(self.cell) cell = (full_cell.T / repeat).T valid = True else: valid = False while not valid: blminmax = max([blmin[k] for k in blmin if k[0] == k[1]]) cell = np.zeros((3, 3)) l = target_volume**0.33 cell[0, 0] = random() * l cell[1, 0] = (random() - 0.5) * cell[0, 0] cell[1, 1] = random() * l cell[2, 0] = (random() - 0.5) * cell[0, 0] cell[2, 1] = (random() - 0.5) * cell[1, 1] cell[2, 2] = random() * l volume = abs(np.linalg.det(cell)) cell *= (target_volume / volume)**0.33 full_cell = (repeat * cell.T).T valid = True if self.cellbounds is not None: if not self.cellbounds.is_within_bounds(full_cell): valid = False for i in range(3): if np.linalg.norm(cell[i, :]) < blminmax: valid = False # generating the atomic positions blocks = [] surplus = [] indices = [] for i, (block, count) in enumerate(self.blocks): count_part = int(np.ceil(count * 1. / nparts)) surplus.append(nparts * count_part - count) blocks.extend([block] * count_part) indices.extend([i] * count_part) N_blocks = len(blocks) # The ordering is shuffled so different blocks # are added in random order. order = list(range(N_blocks)) shuffle(order) blocks = [blocks[i] for i in order] indices = np.array(indices)[order] # Runs until we have found a valid candidate. cand = Atoms('', cell=cell, pbc=pbc) niter = 0 for i in range(N_blocks): atoms = blocks[i].copy() atoms.set_tags(i) rotate = len(atoms) > 1 # Make each new position one at a time. while maxiter is None or niter < maxiter: cop = atoms.get_positions().mean(axis=0) pos = random_pos(cell) atoms.translate(pos - cop) if rotate: phi, theta, psi = 360 * np.random.random(3) atoms.euler_rotate(phi=phi, theta=0.5 * theta, psi=psi, center=pos) # add if it fits: attempt = cand + atoms attempt.wrap() too_close = atoms_too_close(attempt, blmin, use_tags=True) if not too_close: cand += atoms break niter += 1 else: # Reached upper bound on iteration count cand = None break if cand is None: return None # rebuild the candidate after repeating, # randomly deleting surplus blocks and # sorting back to the original order tags = cand.get_tags() nrep = int(np.prod(repeat)) cand_full = cand.repeat(repeat) tags_full = cand_full.get_tags() for i in range(nrep): tags_full[len(cand) * i:len(cand) * (i + 1)] += i * N_blocks cand_full.set_tags(tags_full) cand = Atoms('', cell=full_cell, pbc=pbc) indices_full = np.tile(indices, nrep) tag_counter = 0 for i, (block, count) in enumerate(self.blocks): tags = np.where(indices_full == i)[0] bad = np.random.choice(tags, size=surplus[i], replace=False) for tag in tags: if tag not in bad: select = [a.index for a in cand_full if a.tag == tag] atoms = cand_full[select] # is indeed a copy! atoms.set_tags(tag_counter) assert len(atoms) == len(block) cand += atoms tag_counter += 1 return cand
def mutate(self, atoms): """ Does the actual mutation. """ N = len(atoms) if self.n_top is None else self.n_top slab = atoms[:len(atoms) - N] atoms = atoms[-N:] mutant = atoms.copy() gather_atoms_by_tag(mutant) pos = mutant.get_positions() tags = mutant.get_tags() eligible_tags = tags if self.tags is None else self.tags indices = {} for tag in np.unique(tags): hits = np.where(tags == tag)[0] if len(hits) > 1 and tag in eligible_tags: indices[tag] = hits n_rot = int(np.ceil(len(indices) * self.fraction)) chosen_tags = np.random.choice(list(indices.keys()), size=n_rot, replace=False) too_close = True count = 0 maxcount = 10000 while too_close and count < maxcount: newpos = np.copy(pos) for tag in chosen_tags: p = np.copy(newpos[indices[tag]]) cop = np.mean(p, axis=0) if len(p) == 2: line = (p[1] - p[0]) / np.linalg.norm(p[1] - p[0]) while True: axis = np.random.random(3) axis /= np.linalg.norm(axis) a = np.arccos(np.dot(axis, line)) if np.pi / 4 < a < np.pi * 3 / 4: break else: axis = np.random.random(3) axis /= np.linalg.norm(axis) angle = self.min_angle angle += 2 * (np.pi - self.min_angle) * np.random.random() m = get_rotation_matrix(axis, angle) newpos[indices[tag]] = np.dot(m, (p - cop).T).T + cop mutant.set_positions(newpos) mutant.wrap() too_close = atoms_too_close(mutant, self.blmin, use_tags=True) count += 1 if not too_close and self.test_dist_to_slab: too_close = atoms_too_close_two_sets(slab, mutant, self.blmin) if count == maxcount: mutant = None else: mutant = slab + mutant return mutant
def cross(self, a1, a2, test_dist_to_slab=True): """Crosses the two atoms objects and returns one""" if len(a1) != len(self.slab) + self.n_top: raise ValueError('Wrong size of structure to optimize') if len(a1) != len(a2): raise ValueError('The two structures do not have the same length') N = self.n_top # Only consider the atoms to optimize a1 = a1[len(a1) - N: len(a1)] a2 = a2[len(a2) - N: len(a2)] if not np.array_equal(a1.numbers, a2.numbers): err = 'Trying to pair two structures with different stoichiometry' raise ValueError(err) # Find the common center of the two clusters c1cm = np.average(a1.get_positions(), axis=0) c2cm = np.average(a2.get_positions(), axis=0) cutting_point = (c1cm + c2cm) / 2. counter = 0 too_close = True n_max = 1000 # Run until a valid pairing is made or 1000 pairings are tested. while too_close and counter < n_max: # Generate the cutting plane theta = pi * random() phi = 2. * pi * random() n = (cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta)) n = np.array(n) # Get the pairing top = self._get_pairing_(a1, a2, cutting_plane=n, cutting_point=cutting_point) # Check if the candidate is valid too_close = atoms_too_close(top, self.blmin) if not too_close and test_dist_to_slab: too_close = atoms_too_close_two_sets(self.slab, top, self.blmin) # Verify that the generated structure contains atoms from # both parents n1 = -1 * np.ones((N, )) n2 = -1 * np.ones((N, )) for i in xrange(N): for j in xrange(N): if np.all(a1.positions[j, :] == top.positions[i, :]): n1[i] = j break elif np.all(a2.positions[j, :] == top.positions[i, :]): n2[i] = j break assert (n1[i] > -1 and n2[i] == -1) or (n1[i] == -1 and n2[i] > -1) if not (len(n1[n1 > -1]) > 0 and len(n2[n2 > -1]) > 0): too_close = True counter += 1 if counter == n_max: return None return self.slab + top
def test_chain_operators(seed): from ase.ga.startgenerator import StartGenerator from ase.ga.cutandsplicepairing import CutAndSplicePairing from ase.ga.standardmutations import StrainMutation from ase.ga.utilities import (closest_distances_generator, atoms_too_close, CellBounds) import numpy as np from ase import Atoms # set up the random number generator rng = np.random.RandomState(seed) slab = Atoms('', cell=(0, 16, 16), pbc=[True, False, False]) blocks = ['C'] * 8 n_top = 8 use_tags = False num_vcv = 1 box_volume = 8. * n_top blmin = closest_distances_generator(atom_numbers=[6], ratio_of_covalent_radii=0.6) cellbounds = CellBounds( bounds={ 'phi': [0.1 * 180., 0.9 * 180.], 'chi': [0.1 * 180., 0.9 * 180.], 'psi': [0.1 * 180., 0.9 * 180.], 'a': [1, 6] }) box_to_place_in = [[None, 6., 6.], [None, [0., 4., 0.], [0., 0., 4.]]] sg = StartGenerator(slab, blocks, blmin, box_volume=box_volume, splits=None, box_to_place_in=box_to_place_in, number_of_variable_cell_vectors=num_vcv, cellbounds=cellbounds, test_too_far=True, test_dist_to_slab=False, rng=rng) parents = [] for i in range(2): a = None while a is None: a = sg.get_new_candidate() a.info['confid'] = i parents.append(a) assert len(a) == n_top assert len(np.unique(a.get_tags())) == 8 assert np.allclose(a.get_pbc(), slab.get_pbc()) p = a.get_positions() assert np.min(p[:, 1:]) > 6. assert np.max(p[:, 1:]) < 6. + 4. assert not atoms_too_close(a, blmin, use_tags=use_tags) c = a.get_cell() assert np.allclose(c[1:], slab.get_cell()[1:]) assert cellbounds.is_within_bounds(c) v = a.get_volume() * (4. / 16.)**2 assert abs(v - box_volume) < 1e-5 # Test cut-and-splice pairing and strain mutation pairing = CutAndSplicePairing(slab, n_top, blmin, number_of_variable_cell_vectors=num_vcv, p1=1., p2=0., minfrac=0.15, cellbounds=cellbounds, use_tags=use_tags, rng=rng) strainmut = StrainMutation(blmin, cellbounds=cellbounds, number_of_variable_cell_vectors=num_vcv, use_tags=use_tags, rng=rng) strainmut.update_scaling_volume(parents) for operator in [pairing, strainmut]: child = None while child is None: child, desc = operator.get_new_individual(parents) assert not atoms_too_close(child, blmin, use_tags=use_tags) cell = child.get_cell() assert cellbounds.is_within_bounds(cell) assert np.allclose(cell[1:], slab.get_cell()[1:])
def cross(self, a1, a2): """Crosses the two atoms objects and returns one""" if len(a1) != len(a2): raise ValueError('The two structures do not have the same length') N = len(a1) if self.n_top is None else self.n_top slab = a1[:len(a1) - N] a1 = a1[-N:] a2 = a2[-N:] if not np.array_equal(a1.numbers, a2.numbers): err = 'Trying to pair two structures with different stoichiometry' raise ValueError(err) if self.use_tags and not np.array_equal(a1.get_tags(), a2.get_tags()): err = 'Trying to pair two structures with different tags' raise ValueError(err) a1_copy = a1.copy() a2_copy = a2.copy() if self.cellbounds is not None: if not self.cellbounds.is_within_bounds(a1_copy.get_cell()): niggli_reduce(a1_copy) if not self.cellbounds.is_within_bounds(a2_copy.get_cell()): niggli_reduce(a2_copy) pos1_ref = a1_copy.get_positions() pos2_ref = a2_copy.get_positions() invalid = True counter = 0 maxcount = 1000 # Run until a valid pairing is made or 1000 pairings are tested. while invalid and counter < maxcount: counter += 1 # Choose direction of cutting plane normal (0, 1, or 2): direction = randrange(3) # Randomly translate parent structures: for a, pos in zip([a1_copy, a2_copy], [pos1_ref, pos2_ref]): a.set_positions(pos) cell = a.get_cell() for i in range(3): r = random() cond1 = i == direction and r < self.p1 cond2 = i != direction and r < self.p2 if cond1 or cond2: a.positions += random() * cell[i, :] if self.use_tags: gather_atoms_by_tag(a) else: a.wrap() # Perform the pairing: fraction = random() child = self._get_pairing(a1_copy, a2_copy, direction=direction, fraction=fraction) if child is None: continue # Verify whether the atoms are too close or not: invalid = atoms_too_close(child, self.blmin, use_tags=self.use_tags) if invalid: continue elif self.test_dist_to_slab: invalid = atoms_too_close_two_sets(slab, child, self.blmin) if counter == maxcount: return None return child