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:] 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. """ a = atoms.copy() Natoms = len(a) Nslab = Natoms - self.n_top pos = a.get_positions()[-self.n_top:] num = a.numbers num_top = num[-self.n_top:] num_unique = list(set(num_top)) assert len( num_unique) > 1, 'Permutations with one atomic type is not valid' m = int(ceil(float(self.n_top) * self.probability / 2.)) for _ in range(m): swap_succesfull = False for i in range(100): # Find index pair of atoms to swap i = j = 0 while num[i] == num[j]: i = randrange(Nslab, Natoms) j = randrange(Nslab, Natoms) # Swap two atoms pos_i = a.positions[i].copy() pos_j = a.positions[j].copy() a.positions[i] = pos_j a.positions[j] = pos_i # Check if atoms are too close tc_i = atoms_too_close_two_sets(a[:i] + a[i + 1:], a[i], self.blmin) tc_j = atoms_too_close_two_sets(a[:j] + a[j + 1:], a[j], self.blmin) if tc_i or tc_j: # reset swap a.positions[i] = pos_i a.positions[j] = pos_j continue else: swap_succesfull = True break if not swap_succesfull: # swap_back a.positions[i] = pos_i a.positions[j] = pos_j return a
def mutate(self, atoms): num = atoms.numbers atoms_mutated = atoms.copy() Natoms = len(atoms) Nslab = Natoms - self.n_top for i in range(Nslab, Natoms): if random() < self.rattle_prop: posi_0 = np.copy(atoms_mutated.positions[i]) tc = False tf = False # Random order of atoms atom_indicies = np.random.permutation(np.arange(Nslab, Natoms)) for j in atom_indicies: if j == i: continue posi_j = np.copy(atoms_mutated.positions[j]) r_min = self.blmin[(num[i], num[j])] r_max = 1.7 * r_min for k in range(20): atoms_mutated.positions[i] = posi_j # Then Rattle within a circle r = np.random.uniform(r_min**3, r_max**3)**(1 / 3) theta = np.random.uniform(low=0, high=2 * np.pi) phi = np.random.uniform(low=0, high=np.pi) pos_add = r * np.array([ np.cos(theta) * np.sin(phi), np.sin(theta) * np.sin(phi), np.cos(phi) ]) atoms_mutated.positions[i] += pos_add # too close tc = atoms_too_close_two_sets( atoms_mutated[:i] + atoms_mutated[i + 1:], atoms_mutated[i], self.blmin) # too far index_other = np.delete(np.arange(Natoms), i) blmin_array = np.array( [self.blmin[num[i], num[j]] for j in index_other]) tf = np.min( atoms_mutated.get_distances(i, index_other) / blmin_array) > 1.7 if not tc and not tf: break if tc or tf: atoms_mutated.positions[i] = posi_0 return atoms_mutated
def mutate(self, atoms): num = atoms.numbers atoms_mutated = atoms.copy() Natoms = len(atoms) Nslab = Natoms - self.n_top for i in range(Nslab, Natoms): if random() < self.rattle_prop: posi_0 = np.copy(atoms_mutated.positions[i]) tc = False tf = False for k in range(100): atoms_mutated.positions[i] = posi_0 # Then Rattle within a circle r = self.rattle_strength * np.random.rand()**(1 / 3) theta = np.random.uniform(low=0, high=2 * np.pi) phi = np.random.uniform(low=0, high=np.pi) pos_add = r * np.array([ np.cos(theta) * np.sin(phi), np.sin(theta) * np.sin(phi), np.cos(phi) ]) atoms_mutated.positions[i] += pos_add # Too low - made to avoid that atom is placed below slab if self.min_z is not None: tl = atoms_mutated.positions[i][2] < self.min_z if tl: continue else: tl = False # too close tc = atoms_too_close_two_sets( atoms_mutated[:i] + atoms_mutated[i + 1:], atoms_mutated[i], self.blmin) # too far index_other = np.delete(np.arange(Natoms), i) blmin_array = np.array( [self.blmin[num[i], num[j]] for j in index_other]) tf = np.min( atoms_mutated.get_distances(i, index_other) / blmin_array) > 1.7 if not tc and not tf: break if tc or tf or tl: atoms_mutated.positions[i] = posi_0 return atoms_mutated
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
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 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
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
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 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