def bind(self, frame): if not self.ui.get_widget('/MenuBar/ViewMenu/ShowBonds' ).get_active(): self.bonds = np.empty((0, 5), int) return from ase.atoms import Atoms from ase.neighborlist import NeighborList nl = NeighborList(self.images.r * 1.5, skin=0, self_interaction=False) nl.update(Atoms(positions=self.images.P[frame], cell=(self.images.repeat[:, np.newaxis] * self.images.A[frame]), pbc=self.images.pbc)) nb = nl.nneighbors + nl.npbcneighbors self.bonds = np.empty((nb, 5), int) self.coordination = np.zeros((self.images.natoms), dtype=int) if nb == 0: return n1 = 0 for a in range(self.images.natoms): indices, offsets = nl.get_neighbors(a) self.coordination[a] += len(indices) for a2 in indices: self.coordination[a2] += 1 n2 = n1 + len(indices) self.bonds[n1:n2, 0] = a self.bonds[n1:n2, 1] = indices self.bonds[n1:n2, 2:] = offsets n1 = n2 i = self.bonds[:n2, 2:].any(1) self.bonds[n2:, 0] = self.bonds[i, 1] self.bonds[n2:, 1] = self.bonds[i, 0] self.bonds[n2:, 2:] = -self.bonds[i, 2:]
def candidates_combos(atoms, edge=None, pore_size=None): """Return candidate pore indices combinations.""" from itertools import combinations cans = [] indices = [a.index for a in atoms if a.index not in edge] nblist = NeighborList([graphene_cutoff for i in range(len(atoms))], bothways=True, self_interaction=False) nblist.update(atoms) def constraint_check(pores): for pore in pores: remains = [a.index for a in atoms if a.index not in pore] if is_connected(nblist, remains) and is_connected(nblist, pore): cans.append(pore) if pore_size is not None: pores = combinations(indices, pore_size) print(sum([1 for p in pores])) pores = combinations(indices, pore_size) constraint_check(pores) else: for i in range(1, len(indices)): pores = combinations(indices, i) constraint_check(pores) return cans
def get_bondpairs(atoms, radius=1.1): """Get all pairs of bonding atoms Return all pairs of atoms which are closer than radius times the sum of their respective covalent radii. The pairs are returned as tuples:: (a, b, (i1, i2, i3)) so that atoms a bonds to atom b displaced by the vector:: _ _ _ i c + i c + i c , 1 1 2 2 3 3 where c1, c2 and c3 are the unit cell vectors and i1, i2, i3 are integers.""" from ase.data import covalent_radii from ase.neighborlist import NeighborList cutoffs = radius * covalent_radii[atoms.numbers] nl = NeighborList(cutoffs=cutoffs, self_interaction=False) nl.update(atoms) bondpairs = [] for a in range(len(atoms)): indices, offsets = nl.get_neighbors(a) bondpairs.extend([(a, a2, offset) for a2, offset in zip(indices, offsets)]) return bondpairs
def is_connected(reference, indices, cutoff=graphene_cutoff): """Return True if indices in atoms are connected. Args: reference (Atoms or NeighborList): Structure containing atoms for test. The NeighborList must be constructed with bothways=True. indices (List[int]): Indices of the possibly connected atoms. cutoff (int): Radius defining neighbors in a NeighborList. Only relevent when reference is of the Atoms type. """ if isinstance(reference, Atoms): nblist = NeighborList([cutoff for i in range(len(reference))], bothways=True, self_interaction=False) nblist.update(reference) else: nblist = reference if len(indices) == 0: return True connected = [indices[0]] for c in connected: neighbs = nblist.get_neighbors(c)[0] for n in neighbs: if n in indices and n not in connected: connected.append(n) return set(indices) == set(connected)
def randomize_biatom_13(atoms, type_a, type_b, ratio): """ replace randomly by clusters of 13 atoms to acheive target conc """ n_A = 0 n_B = 0 for atom in atoms: if atom.symbol == type_a: n_A += 1 elif atom.symbol == type_b: n_B += 1 else: raise Exception('Extra chemical element %s!'%atom.symbol) #print n_A, n_B N = len(atoms) nl = NeighborList([1.5]*N, self_interaction=False, bothways=True) # 2*1.5=3 Angstr. radius nl.build(atoms) #print "conc", n_A *1.0 / N r = random.Random() while n_A < ratio*N: # add A atoms randomly index = r.randint(0, N-1) if (atoms[index].symbol != type_a): #print "changing atom #"+str(index)+" to "+type_a #if (r.randint(0, 1000) < 500): atoms[index].symbol = type_a n_A += 1 indeces, offsets = nl.get_neighbors(index) for ia in indeces : if (atoms[ia].symbol != type_a)&(n_A < ratio*N): atoms[ia].symbol = type_a n_A += 1 return atoms
def get_bonds(atoms, covalent_radii): from ase.neighborlist import NeighborList nl = NeighborList(covalent_radii * 1.5, skin=0, self_interaction=False) nl.update(atoms) nbonds = nl.nneighbors + nl.npbcneighbors bonds = np.empty((nbonds, 5), int) if nbonds == 0: return bonds n1 = 0 for a in range(len(atoms)): indices, offsets = nl.get_neighbors(a) n2 = n1 + len(indices) bonds[n1:n2, 0] = a bonds[n1:n2, 1] = indices bonds[n1:n2, 2:] = offsets n1 = n2 i = bonds[:n2, 2:].any(1) pbcbonds = bonds[:n2][i] bonds[n2:, 0] = pbcbonds[:, 1] bonds[n2:, 1] = pbcbonds[:, 0] bonds[n2:, 2:] = -pbcbonds[:, 2:] return bonds
def exafs_first_shell(S02, energy_shift, absorber, ignore_elements, edge, neighbor_cutoff, trajectory): feff_options = { 'RMAX':str(neighbor_cutoff), 'HOLE':'%i %.4f' % (feff_edge_number(edge), S02), 'CORRECTIONS':'%.4f %.4f' % (energy_shift, 0.0), } #get the bulk reference state path = exafs_reference_path(absorber, feff_options) k = None chi_total = None counter = -1 interactions = 0 nl = None for step, atoms in enumerate(trajectory): if COMM_WORLD.rank == 0: time_stamp = strftime("%F %T") print '[%s] step %i/%i' % (time_stamp, step+1, len(trajectory)) atoms = atoms.copy() if ignore_elements: ignore_indicies = [atom.index for atom in atoms if atom.symbol in ignore_elements] del atoms[ignore_indicies] if nl is None: nl = NeighborList(len(atoms)*[neighbor_cutoff], skin=0.3, self_interaction=False) nl.update(atoms) for i in xrange(len(atoms)): if atoms[i].symbol != absorber: continue indicies, offsets = nl.get_neighbors(i) for j, offset in zip(indicies, offsets): counter += 1 if counter % COMM_WORLD.size != COMM_WORLD.rank: continue r = atoms.get_distance(i,j,True) if r >= neighbor_cutoff: continue interactions += 1 k, chi = chi_path(path, r, 0.0, energy_shift, S02, 1) if chi_total is not None: chi_total += chi else: chi_total = chi chi_total = COMM_WORLD.allreduce(chi_total) chi_total /= atoms.get_chemical_symbols().count(absorber) chi_total /= len(trajectory) chi_total *= 2 return k, chi_total
def update(self, atoms): # check all the elements are available in the potential self.Nelements = len(self.elements) elements = np.unique(atoms.get_chemical_symbols()) unavailable = np.logical_not( np.array([item in self.elements for item in elements])) if np.any(unavailable): raise RuntimeError('These elements are not in the potential: %s' % elements[unavailable]) # cutoffs need to be a vector for NeighborList cutoffs = self.cutoff * np.ones(len(atoms)) # convert the elements to an index of the position # in the eam format self.index = np.array([self.elements.index(el) for el in atoms.get_chemical_symbols()]) self.pbc = atoms.get_pbc() # since we need the contribution of all neighbors to the # local electron density we cannot just calculate and use # one way neighbors self.neighbors = NeighborList(cutoffs, skin=self.parameters.skin, self_interaction=False, bothways=True) self.neighbors.update(atoms)
def initialize(self, atoms): self.par = {} self.rc = 0.0 self.numbers = atoms.get_atomic_numbers() if self.parameters.asap_cutoff: relevant_pars = {} for symb, p in parameters.items(): if atomic_numbers[symb] in self.numbers: relevant_pars[symb] = p else: relevant_pars = parameters maxseq = max(par[1] for par in relevant_pars.values()) * Bohr rc = self.rc = beta * maxseq * 0.5 * (sqrt(3) + sqrt(4)) rr = rc * 2 * sqrt(4) / (sqrt(3) + sqrt(4)) self.acut = np.log(9999.0) / (rr - rc) if self.parameters.asap_cutoff: self.rc_list = self.rc * 1.045 else: self.rc_list = self.rc + 0.5 for Z in self.numbers: if Z not in self.par: p = parameters[chemical_symbols[Z]] s0 = p[1] * Bohr eta2 = p[3] / Bohr kappa = p[4] / Bohr x = eta2 * beta * s0 gamma1 = 0.0 gamma2 = 0.0 for i, n in enumerate([12, 6, 24]): r = s0 * beta * sqrt(i + 1) x = n / (12 * (1.0 + exp(self.acut * (r - rc)))) gamma1 += x * exp(-eta2 * (r - beta * s0)) gamma2 += x * exp(-kappa / beta * (r - beta * s0)) self.par[Z] = {'E0': p[0], 's0': s0, 'V0': p[2], 'eta2': eta2, 'kappa': kappa, 'lambda': p[5] / Bohr, 'n0': p[6] / Bohr**3, 'rc': rc, 'gamma1': gamma1, 'gamma2': gamma2} self.ksi = {} for s1, p1 in self.par.items(): self.ksi[s1] = {} for s2, p2 in self.par.items(): self.ksi[s1][s2] = p2['n0'] / p1['n0'] self.forces = np.empty((len(atoms), 3)) self.sigma1 = np.empty(len(atoms)) self.deds = np.empty(len(atoms)) self.nl = NeighborList([0.5 * self.rc_list] * len(atoms), self_interaction=False)
def hop_shuffle(atoms, A, B, count=10, R=3.0): """ Shuffle atoms in given structure by swapping atom types within first coordination shell Parameters ---------- atoms: ase.Atoms ase Atoms object, containing atomic cluster. A, B: string symbols of atoms to swap count: integer number of shuffles R: float radius of coordination shell, were atoms will be swapped Returns ------- Function returns ASE atoms object whith shuffled atoms """ n_atoms = len(atoms) nswaps = 0 neiblist = NeighborList( [ R ] * n_atoms, self_interaction=False, bothways=True ) neiblist.build( atoms ) rnd = random.Random() while nswaps < count: i = rnd.randint(0, n_atoms-1) indeces, offsets = neiblist.get_neighbors( i ) if (atoms[i].symbol == B): candidates = [] for ii in indeces: if atoms[ii].symbol == A: candidates.append( ii ) if len(candidates) > 0: j = random.choice(candidates) atoms[i].symbol = A atoms[j].symbol = B nswaps += 1 neiblist.build( atoms ) elif (atoms[i].symbol == B): candidates = [] for ii in indeces: if atoms[ii].symbol == A: candidates.append( ii ) if len(candidates) > 0: j = random.choice(candidates) atoms[i].symbol = B atoms[j].symbol = A nswaps += 1 neiblist.build( atoms ) return atoms
def calculate(self, atoms=None, properties=['energy'], system_changes=all_changes): Calculator.calculate(self, atoms, properties, system_changes) natoms = len(self.atoms) sigma = self.parameters.sigma epsilon = self.parameters.epsilon rc = self.parameters.rc if rc is None: rc = 3 * sigma if 'numbers' in system_changes: self.nl = NeighborList([rc / 2] * natoms, self_interaction=False) self.nl.update(self.atoms) positions = self.atoms.positions cell = self.atoms.cell e0 = 4 * epsilon * ((sigma / rc)**12 - (sigma / rc)**6) energy = 0.0 forces = np.zeros((natoms, 3)) stress = np.zeros((3, 3)) for a1 in range(natoms): neighbors, offsets = self.nl.get_neighbors(a1) cells = np.dot(offsets, cell) d = positions[neighbors] + cells - positions[a1] r2 = (d**2).sum(1) c6 = (sigma**2 / r2)**3 c6[r2 > rc**2] = 0.0 energy -= e0 * (c6 != 0.0).sum() c12 = c6**2 energy += 4 * epsilon * (c12 - c6).sum() f = (24 * epsilon * (2 * c12 - c6) / r2)[:, np.newaxis] * d forces[a1] -= f.sum(axis=0) for a2, f2 in zip(neighbors, f): forces[a2] += f2 stress += np.dot(f.T, d) if 'stress' in properties: if self.atoms.number_of_lattice_vectors == 3: stress += stress.T.copy() stress *= -0.5 / self.atoms.get_volume() self.results['stress'] = stress.flat[[0, 4, 8, 5, 2, 1]] else: raise PropertyNotImplementedError self.results['energy'] = energy self.results['free_energy'] = energy self.results['forces'] = forces
class TwoCenterHoppingIntegrals: def __init__(self, bopatoms: BOPAtoms, cutoffs: list, **kwargs): self.bopatoms = bopatoms self.nl = NeighborList(cutoffs=cutoffs, bothways=True, self_interaction=False, **kwargs) self.nl.update(bopatoms) def update_hops(self): raise NotImplemented def get_single_hop_global(self, atomindex: int, jneigh: int): hop = self.get_single_hop_local(atomindex, jneigh) rot = self.get_rotation(atomindex, jneigh) return hop def get_single_hop_local(self, atomindex: int, jneigh: int): bopatom_i = self.bopatoms[atomindex] (bopatom_neighbors_i, scaled_pos_list) = self.nl.get_neighbors(atomindex) bopatom_j_neigh_i = self.bopatoms[bopatom_neighbors_i[jneigh]] pos_list = np.dot(scaled_pos_list, self.bopatoms.get_cell()) rel_pos_ij = pos_list[jneigh] r_ij = np.linalg.norm(rel_pos_ij) ss_sigma = 0 sp_sigma = 0 sd_sigma = 0 pp_sigma = 0 pp_pi = 0 pd_sigma = 0 pd_pi = 0 dd_sigma = 0 dd_pi = 0 dd_delta = 0 slater_koster_matrix = np.zeros((9, 9)) # initialize bond integrals if bopatom_i.number_valence_orbitals == 5 and bopatom_j_neigh_i.number_valence_orbitals == 5: dd_sigma = np.exp(-r_ij * 1) dd_pi = np.exp(-r_ij * 2) dd_delta = np.exp(-r_ij * 3) slater_koster_matrix[4, 4] = dd_delta slater_koster_matrix[5, 5] = dd_pi slater_koster_matrix[6, 6] = dd_pi slater_koster_matrix[7, 7] = dd_delta slater_koster_matrix[8, 8] = dd_sigma return slater_koster_matrix[4:, 4:] def get_relative_position(self, index: int, jneigh: int): ''' :param index: atom index :param jneigh: indexing neighboring atoms of atom index :return: ''' # if index > len(self.bopatoms): # raise IndexError # if jneigh > self.nl.nneighbors - 1: # raise IndexError (index_list, relative_position_list) = self.nl.get_neighbors(index) return relative_position_list[jneigh] def get_rotation(self, atomindex: int, jneigh:int, z_axis_global: np.array=np.array([0, 0, 1])) -> Rotation: rel_pos = self.get_relative_position(atomindex, jneigh) v = np.cross(rel_pos, z_axis_global) cosine = np.dot(rel_pos, z_axis_global) if cosine != -1: v_cross = [[0 , -v[2], v[1] ], [v[2] , 0 , -v[0]], [-v[1], v[0] , 0]] rotation_matrix = np.eye(3) + v_cross + np.dot(v_cross, v_cross) / (1 + cosine) else: rotation_matrix = np.diag([1, 1, -1]) return Rotation.from_dcm(rotation_matrix) def get_dbond_rotation_matrix(self, theta: float, phi: float): ''' assumes bond to be ordered in ddsigma, ddpi, ddpi, dddelta :param theta: :param phi: :return: ''' from numpy import sqrt, cos, sin rot = np.zeros((5,5)) rot[4, 3] = -2 * sin(phi) * cos(phi) * cos(theta) rot[3, 3] = cos(phi)**2 - sin(phi)**2 rot[2, 3] = -2 * sin(phi) * cos(phi) * sin(theta) rot[1, 3] = (cos(phi)**2 - sin(phi)**2) * sin(theta) * cos(theta) rot[0, 3] = (cos(phi)**2 - sin(phi)**2) * sqrt(3/4.) * sin(theta)**2 rot[4, 1] = sin(phi) * sin(theta) rot[3, 1] = -cos(phi) * sin(theta) * cos(theta) rot[2, 1] = -sin(phi) * cos(theta) rot[1, 1] = cos(phi) * (cos(phi)**2 - sin(phi)**2) rot[0, 1] = sqrt(3) * cos(phi) * sin(theta) * cos(theta) rot[4, 0] = 0 rot[3, 0] = sqrt(3/4.) * sin(theta)**2 rot[2, 0] = 0 rot[1, 0] = -sqrt(3) * sin(theta) * cos(theta) rot[0, 0] = cos(theta)**2 - 0.5 * sin(theta)**2 rot[4, 2] = -cos(phi) * sin(theta) rot[3, 2] = -sin(phi) * sin(theta) * cos(theta) rot[2, 2] = cos(phi) * cos(theta) rot[1, 2] = sin(phi) * (cos(theta)**2 - sin(theta)**2) rot[0, 2] = sqrt(3) * sin(phi) * sin(theta) * cos(theta) rot[4, 4] = (cos(phi)**2 - sin(phi)**2) * cos(theta) rot[3, 4] = sin(phi) * cos(phi) * (cos(theta)**2 + 1) rot[2, 4] = (cos(phi)**2 - sin(phi)**2) * sin(theta) rot[1, 4] = 2 * sin(phi) * cos(phi) * sin(theta) * cos(theta) rot[0, 4] = sqrt(3) * sin(phi) * cos(phi) * sin(theta)**2 return rot
def _take_fingerprints(self, atoms, individual=False): """ Returns a [fingerprints,typedic] list, where fingerprints is a dictionary with the fingerprints, and typedic is a dictionary with the list of atom indices for each element (or "type") in the atoms object. The keys in the fingerprints dictionary are the (A,B) tuples, which are the different element-element combinations in the atoms object (A and B are the atomic numbers). When A != B, the (A,B) tuple is sorted (A < B). If individual=True, a dict is returned, where each atom index has an {atomic_number:fingerprint} dict as value. If individual=False, the fingerprints from atoms of the same atomic number are added together.""" pos = atoms.get_positions() num = atoms.get_atomic_numbers() cell = atoms.get_cell() unique_types = np.unique(num) posdic = {} typedic = {} for t in unique_types: tlist = [i for i, atom in enumerate(atoms) if atom.number == t] typedic[t] = tlist posdic[t] = pos[tlist] # determining the volume normalization and other parameters volume, pmin, pmax, qmin, qmax = self._get_volume(atoms) # functions for calculating the surface area non_pbc_dirs = [i for i in range(3) if not self.pbc[i]] def surface_area_0d(r): return 4 * np.pi * (r**2) def surface_area_1d(r, pos): q0 = pos[non_pbc_dirs[1]] phi1 = np.lib.scimath.arccos((qmax - q0) / r).real phi2 = np.pi - np.lib.scimath.arccos((qmin - q0) / r).real factor = 1 - (phi1 + phi2) / np.pi return surface_area_2d(r, pos) * factor def surface_area_2d(r, pos): p0 = pos[non_pbc_dirs[0]] area = np.minimum(pmax - p0, r) + np.minimum(p0 - pmin, r) area *= 2 * np.pi * r return area def surface_area_3d(r): return 4 * np.pi * (r**2) # build neighborlist # this is computationally the most intensive part a = atoms.copy() a.set_pbc(self.pbc) nl = NeighborList([self.rcut / 2.] * len(a), skin=0., self_interaction=False, bothways=True) nl.update(a) # parameters for the binning: m = int(np.ceil(self.nsigma * self.sigma / self.binwidth)) x = 0.25 * np.sqrt(2) * self.binwidth * (2 * m + 1) * 1. / self.sigma smearing_norm = erf(x) nbins = int(np.ceil(self.rcut * 1. / self.binwidth)) bindist = self.binwidth * np.arange(1, nbins + 1) def take_individual_rdf(index, unique_type): # Computes the radial distribution function of atoms # of type unique_type around the atom with index "index". rdf = np.zeros(nbins) if self.dimensions == 3: weights = 1. / surface_area_3d(bindist) elif self.dimensions == 2: weights = 1. / surface_area_2d(bindist, pos[index]) elif self.dimensions == 1: weights = 1. / surface_area_1d(bindist, pos[index]) elif self.dimensions == 0: weights = 1. / surface_area_0d(bindist) weights /= self.binwidth indices, offsets = nl.get_neighbors(index) valid = np.where(num[indices] == unique_type) p = pos[indices[valid]] + np.dot(offsets[valid], cell) r = cdist(p, [pos[index]]) bins = np.floor(r / self.binwidth) for i in range(-m, m + 1): newbins = bins + i valid = np.where((newbins >= 0) & (newbins < nbins)) valid_bins = newbins[valid].astype(int) values = weights[valid_bins] c = 0.25 * np.sqrt(2) * self.binwidth * 1. / self.sigma values *= 0.5 * erf(c * (2 * i + 1)) - \ 0.5 * erf(c * (2 * i - 1)) values /= smearing_norm for j, valid_bin in enumerate(valid_bins): rdf[valid_bin] += values[j] rdf /= len(typedic[unique_type]) * 1. / volume return rdf fingerprints = {} if individual: for i in range(len(atoms)): fingerprints[i] = {} for unique_type in unique_types: fingerprint = take_individual_rdf(i, unique_type) if self.dimensions > 0: fingerprint -= 1 fingerprints[i][unique_type] = fingerprint else: for t1, t2 in combinations_with_replacement(unique_types, r=2): key = (t1, t2) fingerprint = np.zeros(nbins) for i in typedic[t1]: fingerprint += take_individual_rdf(i, t2) fingerprint /= len(typedic[t1]) if self.dimensions > 0: fingerprint -= 1 fingerprints[key] = fingerprint return [fingerprints, typedic]
class LennardJones(Calculator): """Lennard Jones potential calculator see https://en.wikipedia.org/wiki/Lennard-Jones_potential The fundamental definition of this potential is a pairwise energy: ``u_ij = 4 epsilon ( sigma^12/r_ij^12 - sigma^6/r_ij^6 )`` For convenience, we'll use d_ij to refer to "distance vector" and ``r_ij`` to refer to "scalar distance". So, with position vectors `r_i`: ``r_ij = | r_j - r_i | = | d_ij |`` The derivative of u_ij is: :: d u_ij / d r_ij = (-24 epsilon / r_ij) ( sigma^12/r_ij^12 - sigma^6/r_ij^6 ) We can define a pairwise force ``f_ij = d u_ij / d d_ij = d u_ij / d r_ij * d_ij / r_ij`` The terms in front of d_ij are often combined into a "general derivative" ``du_ij = (d u_ij / d d_ij) / r_ij`` The force on an atom is: ``f_i = sum_(j != i) f_ij`` There is some freedom of choice in assigning atomic energies, i.e. choosing a way to partition the total energy into atomic contributions. We choose a symmetric approach: ``u_i = 1/2 sum_(j != i) u_ij`` The total energy of a system of atoms is then: ``u = sum_i u_i = 1/2 sum_(i, j != i) u_ij`` The stress can be written as ( `(x)` denoting outer product): ``sigma = 1/2 sum_(i, j != i) f_ij (x) d_ij = sum_i sigma_i ,`` with atomic contributions ``sigma_i = 1/2 sum_(j != i) f_ij (x) d_ij`` Implementation note: For computational efficiency, we minimise the number of pairwise evaluations, so we iterate once over all the atoms, and use NeighbourList with bothways=False. In terms of the equations, we therefore effectively restrict the sum over `i != j` to `j > i`, and need to manually re-add the "missing" `j < i` contributions. Another consideration is the cutoff. We have to ensure that the potential goes to zero smoothly as an atom moves across the cutoff threshold, otherwise the potential is not continuous. In cases where the cutoff is so large that u_ij is very small at the cutoff this is automatically ensured, but in general, `u_ij(rc) != 0`. In order to catch this case, this implementation shifts the total energy ``u'_ij = u_ij - u_ij(rc)`` which ensures that it is precisely zero at the cutoff. However, this means that the energy effectively depends on the cutoff, which might lead to unexpected results! """ implemented_properties = [ 'energy', 'atomic_energies', 'forces', 'free_energy' ] implemented_properties += ['stress', 'stresses'] # bulk properties default_parameters = {'epsilon': 1.0, 'sigma': 1.0, 'rc': None} nolabel = True def __init__(self, **kwargs): """ Parameters ---------- sigma: float The potential minimum is at 2**(1/6) * sigma, default 1.0 epsilon: float The potential depth, default 1.0 rc: float, None Cut-off for the NeighborList is set to 3 * sigma if None. The energy is upshifted to be continuous at rc. Default None """ Calculator.__init__(self, **kwargs) if self.parameters.rc is None: self.parameters.rc = 3 * self.parameters.sigma self.nl = None def calculate( self, atoms=None, properties=None, system_changes=all_changes, ): if properties is None: properties = self.implemented_properties Calculator.calculate(self, atoms, properties, system_changes) natoms = len(self.atoms) sigma = self.parameters.sigma epsilon = self.parameters.epsilon rc = self.parameters.rc if self.nl is None or 'numbers' in system_changes: self.nl = NeighborList([rc / 2] * natoms, self_interaction=False) self.nl.update(self.atoms) positions = self.atoms.positions cell = self.atoms.cell # potential value at rc e0 = 4 * epsilon * ((sigma / rc)**12 - (sigma / rc)**6) atomic_energies = np.zeros(natoms) forces = np.zeros((natoms, 3)) stresses = np.zeros((natoms, 3, 3)) for ii in range(natoms): neighbors, offsets = self.nl.get_neighbors(ii) cells = np.dot(offsets, cell) # pointing *towards* neighbours distance_vectors = positions[neighbors] + cells - positions[ii] r2 = (distance_vectors**2).sum(1) c6 = (sigma**2 / r2)**3 c6[r2 > rc**2] = 0.0 c12 = c6**2 pairwise_energies = 4 * epsilon * (c12 - c6) - e0 * (c6 != 0.0) atomic_energies[ii] += 0.5 * pairwise_energies.sum( ) # atomic energies pairwise_forces = (-24 * epsilon * (2 * c12 - c6) / r2)[:, np.newaxis] * distance_vectors forces[ii] += pairwise_forces.sum(axis=0) stresses[ii] += 0.5 * np.dot( pairwise_forces.T, distance_vectors) # equivalent to outer product # add j < i contributions for jj, atom_j in enumerate(neighbors): atomic_energies[atom_j] += 0.5 * pairwise_energies[jj] forces[atom_j] += -pairwise_forces[jj] # f_ji = - f_ij stresses[atom_j] += 0.5 * np.outer(pairwise_forces[jj], distance_vectors[jj]) # no lattice, no stress if self.atoms.number_of_lattice_vectors == 3: stresses = full_3x3_to_voigt_6_stress(stresses) self.results['stress'] = (stresses.sum(axis=0) / self.atoms.get_volume()) self.results['stresses'] = stresses / self.atoms.get_volume() energy = atomic_energies.sum() self.results['energy'] = energy self.results['atomic_energies'] = atomic_energies self.results['free_energy'] = energy self.results['forces'] = forces
def initialize(self, atoms): self.par = {} self.rc = 0.0 self.numbers = atoms.get_atomic_numbers() if self.parameters.asap_cutoff: relevant_pars = {} for symb, p in parameters.items(): if atomic_numbers[symb] in self.numbers: relevant_pars[symb] = p else: relevant_pars = parameters maxseq = max(par[1] for par in relevant_pars.values()) * Bohr rc = self.rc = beta * maxseq * 0.5 * (sqrt(3) + sqrt(4)) rr = rc * 2 * sqrt(4) / (sqrt(3) + sqrt(4)) self.acut = np.log(9999.0) / (rr - rc) if self.parameters.asap_cutoff: self.rc_list = self.rc * 1.045 else: self.rc_list = self.rc + 0.5 for Z in self.numbers: if Z not in self.par: sym = chemical_symbols[Z] if sym not in parameters: raise NotImplementedError( 'No EMT-potential for {0}'.format(sym)) p = parameters[sym] s0 = p[1] * Bohr eta2 = p[3] / Bohr kappa = p[4] / Bohr x = eta2 * beta * s0 gamma1 = 0.0 gamma2 = 0.0 for i, n in enumerate([12, 6, 24]): r = s0 * beta * sqrt(i + 1) x = n / (12 * (1.0 + exp(self.acut * (r - rc)))) gamma1 += x * exp(-eta2 * (r - beta * s0)) gamma2 += x * exp(-kappa / beta * (r - beta * s0)) self.par[Z] = { 'E0': p[0], 's0': s0, 'V0': p[2], 'eta2': eta2, 'kappa': kappa, 'lambda': p[5] / Bohr, 'n0': p[6] / Bohr**3, 'rc': rc, 'gamma1': gamma1, 'gamma2': gamma2 } self.ksi = {} for s1, p1 in self.par.items(): self.ksi[s1] = {} for s2, p2 in self.par.items(): self.ksi[s1][s2] = p2['n0'] / p1['n0'] self.forces = np.empty((len(atoms), 3)) self.stress = np.empty((3, 3)) self.sigma1 = np.empty(len(atoms)) self.deds = np.empty(len(atoms)) self.nl = NeighborList([0.5 * self.rc_list] * len(atoms), self_interaction=False)
def insertHbyList(ase_struct, pmd_top, implicitHbondingPartners, bond_length=1.0, debug=False): # make copies of passed structures as not to alter originals: new_pmd_top = pmd_top.copy(pmd.Structure) new_ase_struct = ase_struct.copy() # names stores the String IDs of all atoms as to facilitate later ASE ID -> Atom name mapping names = [a.name for a in pmd_top.atoms] residues = [a.residue.name for a in pmd_top.atoms] # make copied atoms accessible by unchangable indices (standard list) originalAtoms = [a for a in new_pmd_top.atoms] implicitHbondingPartnersIdxHnoTuples = [ (a.idx, implicitHbondingPartners[k]) for a in pmd_top.atoms for k in implicitHbondingPartners.keys() if a.name == k ] implicitHbondingPartnersIdxHnoDict = dict( implicitHbondingPartnersIdxHnoTuples) # build numbered neighbour list "manually" #i: list of atoms e.g. [0,0,0,1,1,2,2,...] i = np.array([b.atom1.idx for b in new_pmd_top.bonds]) #j: list of bonding partners corresponding to i [1,2,3,...] ==> 0 has bondpartners 1,2 and 3; etc. j = np.array([b.atom2.idx for b in new_pmd_top.bonds]) r = new_ase_struct.positions for k, Hno in implicitHbondingPartnersIdxHnoDict.items( ): # for all atoms to append hydrogen to logging.info('Adding {} H-atoms to {} (#{})...'.format( Hno, originalAtoms[k].name, k)) for h in range(0, Hno): r = new_ase_struct.positions bondingPartners = j[i == k] logging.info('bondingPartners {}'.format(bondingPartners)) partnerStr = '' for p in bondingPartners: if partnerStr == '': partnerStr = originalAtoms[p].name else: partnerStr += ', ' + originalAtoms[p].name logging.info('Atom {} already has bonding partners {}'.format( originalAtoms[k].name, partnerStr)) dr = (r[j[i == k]] - r[k]).mean(axis=0) # my understanding: dr is vector # from atom k's position towards the geometrical center of mass # it forms with its defined neighbours # r0 is a vector offset into the opposit direction: dr = dr / np.linalg.norm(dr) #normalized vector in direction dr #calculate an orthogonal vector 'dr_ortho' on dr #and push the H atoms in dr+dr_ortho and dr-dr_ortho #if one has to add more than two H atoms introduce dr_ortho_2 = dr x dr_ortho dr_ortho = np.cross(dr, np.array([1, 0, 0])) if np.linalg.norm( dr_ortho ) < 0.1: #if dr and (1,0,0) have almost the same direction dr_ortho = np.cross(dr, np.array([0, 1, 0])) # (1-2*h) = {1,-1} for h={0,1} h_pos_vec = (dr + (1 - 2 * h) * dr_ortho ) / np.linalg.norm(dr + (1 - 2 * h) * dr_ortho) r0 = r[k] - bond_length * h_pos_vec new_ase_struct += ase.Atom('H', r0) # add atom in ase structure n_atoms = len(new_ase_struct) #introduce a corrector step for a added atom which is too close to others #do as many corrector stepps until all atoms are more than 1\AA appart c_step = 0 while True: nl = NeighborList(cutoffs=[.5] * len(new_ase_struct), skin=0.09, self_interaction=False, bothways=True) nl.update(new_ase_struct) indices, offsets = nl.get_neighbors(-1) indices = np.delete(indices, np.where(indices == k)) if len(indices) == 0: break elif c_step > 15: logging.info( 'programm needs more than 15 corrector steps for H atom {} at atom {}' .format(n_atoms, k)) sys.exit(15) break logging.info('too close atoms {}'.format(indices)) c_step += 1 logging.info('correcter step {} for H {} at atom {}'.format( c_step, n_atoms - 1, k)) # if indices not empty -> the atom(-1)=r_H is to close together # with atom a_close=indices[0], it is a H-atom belonging to atom 'k'=r_k . #correctorstep: corr_step = (r_H-a_close)/|(r_H-a_close)| #corrected_pos: corr_pos = ((r_H-r_k) + corr_step)/|((r_H-r_k) + corr_step)| #new H position: new_r_H = r_k + corr_pos r_H, r_k, a_close = np.take(new_ase_struct.get_positions(), [-1, k, indices[0]], axis=0) #print('r_H, r_k, a_close', r_H, r_k, a_close) corr_step = (r_H - a_close) / np.linalg.norm( (r_H - a_close) ) #maybe introduce here a skaling Faktor s=0.3 or somthing like that to make tiny corrections and don't overshoot. corr_pos = ((r_H - r_k) + corr_step) / np.linalg.norm( (r_H - r_k) + corr_step) new_r_H = r_k + bond_length * corr_pos #correct the H position to new_r_H in new_ase_struct trans = np.zeros([n_atoms, 3]) trans[-1] = new_r_H - r_H new_ase_struct.translate(trans) #view(new_ase_struct) #sys.exit() i = np.append(i, k) # manually update numbered neighbour lists j = np.append(j, len(new_ase_struct) - 1) # update pmd topology bondingPartner = originalAtoms[ k] # here we need the original numbering, # as ParmEd alters indices when adding atoms to the structure nameH = '{}{}'.format( h + 1, bondingPartner.name) # atom needs a unique name logging.info('Adding H-atom {} at position [ {}, {}, {} ]'.format( nameH, r0[0], r0[1], r0[2])) new_H = pmd.Atom(name=nameH, type='H', atomic_number=1) new_H.xx = r0[ 0] # ParmEd documentation not very helpful, did not find any more compact assignment new_H.xy = r0[1] new_H.xz = r0[2] # do not understand ParmEd that well, apparently we need the Bond object in order to update topology new_Bond = pmd.Bond(bondingPartner, new_H) new_H.bond_to( bondingPartner) # not sure, whether this is necessary new_pmd_top.bonds.append(new_Bond) new_pmd_top.add_atom_to_residue(new_H, bondingPartner.residue) originalAtoms.append( new_H) # add atom to the bottom of "index-stiff" list names.append(nameH) # append name of H-atom residues.append(bondingPartner.residue.name ) # H is in same residue as bonding partner return new_ase_struct, new_pmd_top, names, residues
class OPLSff: def __init__(self, fileobj=None, warnings=0): self.warnings = warnings self.data = {} if fileobj is not None: self.read(fileobj) def read(self, fileobj, comments='#'): if isinstance(fileobj, str): fileobj = open(fileobj) def read_block(name, symlen, nvalues): """Read a data block. name: name of the block to store in self.data symlen: length of the symbol nvalues: number of values expected """ if name not in self.data: self.data[name] = {} data = self.data[name] def add_line(): line = fileobj.readline().strip() if not len(line): # end of the block return False line = line.split('#')[0] # get rid of comments if len(line) > symlen: symbol = line[:symlen] words = line[symlen:].split() if len(words) >= nvalues: if nvalues == 1: data[symbol] = float(words[0]) else: data[symbol] = [float(word) for word in words[:nvalues]] return True while add_line(): pass read_block('one', 2, 3) read_block('bonds', 5, 2) read_block('angles', 8, 2) read_block('dihedrals', 11, 4) read_block('cutoffs', 5, 1) self.bonds = BondData(self.data['bonds']) self.angles = AnglesData(self.data['angles']) self.dihedrals = DihedralsData(self.data['dihedrals']) self.cutoffs = CutoffList(self.data['cutoffs']) def write_lammps(self, atoms, prefix='lammps'): """Write input for a LAMMPS calculation.""" self.prefix = prefix if hasattr(atoms, 'connectivities'): connectivities = atoms.connectivities else: btypes, blist = self.get_bonds(atoms) atypes, alist = self.get_angles() dtypes, dlist = self.get_dihedrals(alist, atypes) connectivities = { 'bonds': blist, 'bond types': btypes, 'angles': alist, 'angle types': atypes, 'dihedrals': dlist, 'dihedral types': dtypes, } self.write_lammps_definitions(atoms, btypes, atypes, dtypes) self.write_lammps_in() self.write_lammps_atoms(atoms, connectivities) def write_lammps_in(self): fileobj = self.prefix + '_in' if isinstance(fileobj, str): fileobj = open(fileobj, 'w') fileobj.write("""# LAMMPS relaxation (written by ASE) units metal atom_style full boundary p p p #boundary p p f """) fileobj.write('read_data ' + self.prefix + '_atoms\n') fileobj.write('include ' + self.prefix + '_opls\n') fileobj.write(""" kspace_style pppm 1e-5 #kspace_modify slab 3.0 neighbor 1.0 bin neigh_modify delay 0 every 1 check yes thermo 1000 thermo_style custom step temp press cpu pxx pyy pzz pxy pxz pyz ke pe etotal vol lx ly lz atoms dump 1 all xyz 1000 dump_relax.xyz dump_modify 1 sort id restart 100000 test_relax min_style fire minimize 1.0e-14 1.0e-5 100000 100000 """) fileobj.close() def write_lammps_atoms(self, atoms, connectivities): """Write atoms input for LAMMPS""" fname = self.prefix + '_atoms' fileobj = open(fname, 'w') # header fileobj.write(fileobj.name + ' (by ' + str(self.__class__) + ')\n\n') fileobj.write(str(len(atoms)) + ' atoms\n') fileobj.write(str(len(atoms.types)) + ' atom types\n') blist = connectivities['bonds'] if len(blist): btypes = connectivities['bond types'] fileobj.write(str(len(blist)) + ' bonds\n') fileobj.write(str(len(btypes)) + ' bond types\n') alist = connectivities['angles'] if len(alist): atypes = connectivities['angle types'] fileobj.write(str(len(alist)) + ' angles\n') fileobj.write(str(len(atypes)) + ' angle types\n') dlist = connectivities['dihedrals'] if len(dlist): dtypes = connectivities['dihedral types'] fileobj.write(str(len(dlist)) + ' dihedrals\n') fileobj.write(str(len(dtypes)) + ' dihedral types\n') # cell p = Prism(atoms.get_cell()) xhi, yhi, zhi, xy, xz, yz = p.get_lammps_prism_str() fileobj.write('\n0.0 %s xlo xhi\n' % xhi) fileobj.write('0.0 %s ylo yhi\n' % yhi) fileobj.write('0.0 %s zlo zhi\n' % zhi) # atoms fileobj.write('\nAtoms\n\n') tag = atoms.get_tags() if atoms.has('molid'): molid = atoms.get_array('molid') else: molid = [1] * len(atoms) for i, r in enumerate(map(p.pos_to_lammps_str, atoms.get_positions())): q = self.data['one'][atoms.types[tag[i]]][2] fileobj.write('%6d %3d %3d %s %s %s %s' % ((i + 1, molid[i], tag[i] + 1, q) + tuple(r))) fileobj.write(' # ' + atoms.types[tag[i]] + '\n') # velocities velocities = atoms.get_velocities() if velocities is not None: fileobj.write('\nVelocities\n\n') for i, v in enumerate(velocities): fileobj.write('%6d %g %g %g\n' % (i + 1, v[0], v[1], v[2])) # masses fileobj.write('\nMasses\n\n') for i, typ in enumerate(atoms.types): cs = atoms.split_symbol(typ)[0] fileobj.write('%6d %g # %s -> %s\n' % (i + 1, atomic_masses[chemical_symbols.index(cs)], typ, cs)) # bonds if len(blist): fileobj.write('\nBonds\n\n') for ib, bvals in enumerate(blist): fileobj.write('%8d %6d %6d %6d ' % (ib + 1, bvals[0] + 1, bvals[1] + 1, bvals[2] + 1)) try: fileobj.write('# ' + btypes[bvals[0]]) except: pass fileobj.write('\n') # angles if len(alist): fileobj.write('\nAngles\n\n') for ia, avals in enumerate(alist): fileobj.write('%8d %6d %6d %6d %6d ' % (ia + 1, avals[0] + 1, avals[1] + 1, avals[2] + 1, avals[3] + 1)) try: fileobj.write('# ' + atypes[avals[0]]) except: pass fileobj.write('\n') # dihedrals if len(dlist): fileobj.write('\nDihedrals\n\n') for i, dvals in enumerate(dlist): fileobj.write('%8d %6d %6d %6d %6d %6d ' % (i + 1, dvals[0] + 1, dvals[1] + 1, dvals[2] + 1, dvals[3] + 1, dvals[4] + 1)) try: fileobj.write('# ' + dtypes[dvals[0]]) except: pass fileobj.write('\n') def update_neighbor_list(self, atoms): cut = 0.5 * max(self.data['cutoffs'].values()) self.nl = NeighborList([cut] * len(atoms), skin=0, bothways=True, self_interaction=False) self.nl.update(atoms) self.atoms = atoms def get_bonds(self, atoms): """Find bonds and return them and their types""" cutoffs = CutoffList(self.data['cutoffs']) self.update_neighbor_list(atoms) types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() bond_list = [] bond_types = [] for i, atom in enumerate(atoms): iname = types[tags[i]] indices, offsets = self.nl.get_neighbors(i) for j, offset in zip(indices, offsets): if j <= i: continue # do not double count jname = types[tags[j]] cut = cutoffs.value(iname, jname) if cut is None: if self.warnings > 1: print('Warning: cutoff %s-%s not found' % (iname, jname)) continue # don't have it dist = np.linalg.norm(atom.position - atoms[j].position - np.dot(offset, cell)) if dist > cut: continue # too far away name, val = self.bonds.name_value(iname, jname) if name is None: if self.warnings: print('Warning: potential %s-%s not found' % (iname, jname)) continue # don't have it if name not in bond_types: bond_types.append(name) bond_list.append([bond_types.index(name), i, j]) return bond_types, bond_list def get_angles(self, atoms=None): cutoffs = CutoffList(self.data['cutoffs']) if atoms is not None: self.update_neighbor_list(atoms) else: atoms = self.atoms types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() ang_list = [] ang_types = [] # center atom *-i-* for i, atom in enumerate(atoms): iname = types[tags[i]] indicesi, offsetsi = self.nl.get_neighbors(i) # search for first neighbor j-i-* for j, offsetj in zip(indicesi, offsetsi): jname = types[tags[j]] cut = cutoffs.value(iname, jname) if cut is None: continue # don't have it dist = np.linalg.norm(atom.position - atoms[j].position - np.dot(offsetj, cell)) if dist > cut: continue # too far away # search for second neighbor j-i-k for k, offsetk in zip(indicesi, offsetsi): if k <= j: continue # avoid double count kname = types[tags[k]] cut = cutoffs.value(iname, kname) if cut is None: continue # don't have it dist = np.linalg.norm(atom.position - np.dot(offsetk, cell) - atoms[k].position) if dist > cut: continue # too far away name, val = self.angles.name_value(jname, iname, kname) if name is None: if self.warnings > 1: print('Warning: angles %s-%s-%s not found' % (jname, iname, kname)) continue # don't have it if name not in ang_types: ang_types.append(name) ang_list.append([ang_types.index(name), j, i, k]) return ang_types, ang_list def get_dihedrals(self, ang_types, ang_list): 'Dihedrals derived from angles.' cutoffs = CutoffList(self.data['cutoffs']) atoms = self.atoms types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() dih_list = [] dih_types = [] def append(name, i, j, k, l): if name not in dih_types: dih_types.append(name) index = dih_types.index(name) if (([index, i, j, k, l] not in dih_list) and ([index, l, k, j, i] not in dih_list)): dih_list.append([index, i, j, k, l]) for angle in ang_types: l, i, j, k = angle iname = types[tags[i]] jname = types[tags[j]] kname = types[tags[k]] # search for l-i-j-k indicesi, offsetsi = self.nl.get_neighbors(i) for l, offsetl in zip(indicesi, offsetsi): if l == j: continue # avoid double count lname = types[tags[l]] cut = cutoffs.value(iname, lname) if cut is None: continue # don't have it dist = np.linalg.norm(atoms[i].position - atoms[l].position - np.dot(offsetl, cell)) if dist > cut: continue # too far away name, val = self.dihedrals.name_value(lname, iname, jname, kname) if name is None: continue # don't have it append(name, l, i, j, k) # search for i-j-k-l indicesk, offsetsk = self.nl.get_neighbors(k) for l, offsetl in zip(indicesk, offsetsk): if l == j: continue # avoid double count lname = types[tags[l]] cut = cutoffs.value(kname, lname) if cut is None: continue # don't have it dist = np.linalg.norm(atoms[k].position - atoms[l].position - np.dot(offsetl, cell)) if dist > cut: continue # too far away name, val = self.dihedrals.name_value(iname, jname, kname, lname) if name is None: continue # don't have it append(name, i, j, k, l) return dih_types, dih_list def write_lammps_definitions(self, atoms, btypes, atypes, dtypes): """Write force field definitions for LAMMPS.""" fileobj = self.prefix + '_opls' if isinstance(fileobj, str): fileobj = open(fileobj, 'w') fileobj.write('# OPLS potential\n') fileobj.write('# write_lammps' + str(time.asctime( time.localtime(time.time())))) # bonds if len(btypes): fileobj.write('\n# bonds\n') fileobj.write('bond_style harmonic\n') for ib, btype in enumerate(btypes): fileobj.write('bond_coeff %6d' % (ib + 1)) for value in self.bonds.nvh[btype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + btype + '\n') # angles if len(atypes): fileobj.write('\n# angles\n') fileobj.write('angle_style harmonic\n') for ia, atype in enumerate(atypes): fileobj.write('angle_coeff %6d' % (ia + 1)) for value in self.angles.nvh[atype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + atype + '\n') # dihedrals if len(dtypes): fileobj.write('\n# dihedrals\n') fileobj.write('dihedral_style opls\n') for i, dtype in enumerate(dtypes): fileobj.write('dihedral_coeff %6d' % (i + 1)) for value in self.dihedrals.nvh[dtype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + dtype + '\n') # Lennard Jones settings fileobj.write('\n# L-J parameters\n') fileobj.write('pair_style lj/cut/coul/long 10.0 7.4' + ' # consider changing these parameters\n') fileobj.write('special_bonds lj/coul 0.0 0.0 0.5\n') data = self.data['one'] for ia, atype in enumerate(atoms.types): if len(atype) < 2: atype = atype + ' ' fileobj.write('pair_coeff ' + str(ia + 1) + ' ' + str(ia + 1)) for value in data[atype][:2]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + atype + '\n') fileobj.write('pair_modify shift yes mix geometric\n') # Charges fileobj.write('\n# charges\n') for ia, atype in enumerate(atoms.types): if len(atype) < 2: atype = atype + ' ' fileobj.write('set type ' + str(ia + 1)) fileobj.write(' charge ' + str(data[atype][2])) fileobj.write(' # ' + atype + '\n')
class EMT(Calculator): """Python implementation of the Effective Medium Potential. Supports the following standard EMT metals: Al, Cu, Ag, Au, Ni, Pd and Pt. In addition, the following elements are supported. They are NOT well described by EMT, and the parameters are not for any serious use: H, C, N, O The potential takes a single argument, ``asap_cutoff`` (default: False). If set to True, the cutoff mimics how Asap does it; most importantly the global cutoff is chosen from the largest atom present in the simulation, if False it is chosen from the largest atom in the parameter table. True gives the behaviour of the Asap code and older EMT implementations, although the results are not bitwise identical. """ implemented_properties = ['energy', 'forces'] nolabel = True default_parameters = {'asap_cutoff': False} def __init__(self, **kwargs): Calculator.__init__(self, **kwargs) def initialize(self, atoms): self.par = {} self.rc = 0.0 self.numbers = atoms.get_atomic_numbers() if self.parameters.asap_cutoff: relevant_pars = {} for symb, p in parameters.items(): if atomic_numbers[symb] in self.numbers: relevant_pars[symb] = p else: relevant_pars = parameters maxseq = max(par[1] for par in relevant_pars.values()) * Bohr rc = self.rc = beta * maxseq * 0.5 * (sqrt(3) + sqrt(4)) rr = rc * 2 * sqrt(4) / (sqrt(3) + sqrt(4)) self.acut = np.log(9999.0) / (rr - rc) if self.parameters.asap_cutoff: self.rc_list = self.rc * 1.045 else: self.rc_list = self.rc + 0.5 for Z in self.numbers: if Z not in self.par: sym = chemical_symbols[Z] if sym not in parameters: raise NotImplementedError('No EMT-potential for {0}' .format(sym)) p = parameters[sym] s0 = p[1] * Bohr eta2 = p[3] / Bohr kappa = p[4] / Bohr x = eta2 * beta * s0 gamma1 = 0.0 gamma2 = 0.0 for i, n in enumerate([12, 6, 24]): r = s0 * beta * sqrt(i + 1) x = n / (12 * (1.0 + exp(self.acut * (r - rc)))) gamma1 += x * exp(-eta2 * (r - beta * s0)) gamma2 += x * exp(-kappa / beta * (r - beta * s0)) self.par[Z] = {'E0': p[0], 's0': s0, 'V0': p[2], 'eta2': eta2, 'kappa': kappa, 'lambda': p[5] / Bohr, 'n0': p[6] / Bohr**3, 'rc': rc, 'gamma1': gamma1, 'gamma2': gamma2} self.ksi = {} for s1, p1 in self.par.items(): self.ksi[s1] = {} for s2, p2 in self.par.items(): self.ksi[s1][s2] = p2['n0'] / p1['n0'] self.forces = np.empty((len(atoms), 3)) self.sigma1 = np.empty(len(atoms)) self.deds = np.empty(len(atoms)) self.nl = NeighborList([0.5 * self.rc_list] * len(atoms), self_interaction=False) def calculate(self, atoms=None, properties=['energy'], system_changes=all_changes): Calculator.calculate(self, atoms, properties, system_changes) if 'numbers' in system_changes: self.initialize(self.atoms) positions = self.atoms.positions numbers = self.atoms.numbers cell = self.atoms.cell self.nl.update(self.atoms) self.energy = 0.0 self.sigma1[:] = 0.0 self.forces[:] = 0.0 natoms = len(self.atoms) for a1 in range(natoms): Z1 = numbers[a1] p1 = self.par[Z1] ksi = self.ksi[Z1] neighbors, offsets = self.nl.get_neighbors(a1) offsets = np.dot(offsets, cell) for a2, offset in zip(neighbors, offsets): d = positions[a2] + offset - positions[a1] r = sqrt(np.dot(d, d)) if r < self.rc_list: Z2 = numbers[a2] p2 = self.par[Z2] self.interact1(a1, a2, d, r, p1, p2, ksi[Z2]) for a in range(natoms): Z = numbers[a] p = self.par[Z] try: ds = -log(self.sigma1[a] / 12) / (beta * p['eta2']) except (OverflowError, ValueError): self.deds[a] = 0.0 self.energy -= p['E0'] continue x = p['lambda'] * ds y = exp(-x) z = 6 * p['V0'] * exp(-p['kappa'] * ds) self.deds[a] = ((x * y * p['E0'] * p['lambda'] + p['kappa'] * z) / (self.sigma1[a] * beta * p['eta2'])) self.energy += p['E0'] * ((1 + x) * y - 1) + z for a1 in range(natoms): Z1 = numbers[a1] p1 = self.par[Z1] ksi = self.ksi[Z1] neighbors, offsets = self.nl.get_neighbors(a1) offsets = np.dot(offsets, cell) for a2, offset in zip(neighbors, offsets): d = positions[a2] + offset - positions[a1] r = sqrt(np.dot(d, d)) if r < self.rc_list: Z2 = numbers[a2] p2 = self.par[Z2] self.interact2(a1, a2, d, r, p1, p2, ksi[Z2]) self.results['energy'] = self.energy self.results['free_energy'] = self.energy self.results['forces'] = self.forces def interact1(self, a1, a2, d, r, p1, p2, ksi): x = exp(self.acut * (r - self.rc)) theta = 1.0 / (1.0 + x) y1 = (0.5 * p1['V0'] * exp(-p2['kappa'] * (r / beta - p2['s0'])) * ksi / p1['gamma2'] * theta) y2 = (0.5 * p2['V0'] * exp(-p1['kappa'] * (r / beta - p1['s0'])) / ksi / p2['gamma2'] * theta) self.energy -= y1 + y2 f = ((y1 * p2['kappa'] + y2 * p1['kappa']) / beta + (y1 + y2) * self.acut * theta * x) * d / r self.forces[a1] += f self.forces[a2] -= f self.sigma1[a1] += (exp(-p2['eta2'] * (r - beta * p2['s0'])) * ksi * theta / p1['gamma1']) self.sigma1[a2] += (exp(-p1['eta2'] * (r - beta * p1['s0'])) / ksi * theta / p2['gamma1']) def interact2(self, a1, a2, d, r, p1, p2, ksi): x = exp(self.acut * (r - self.rc)) theta = 1.0 / (1.0 + x) y1 = (exp(-p2['eta2'] * (r - beta * p2['s0'])) * ksi / p1['gamma1'] * theta * self.deds[a1]) y2 = (exp(-p1['eta2'] * (r - beta * p1['s0'])) / ksi / p2['gamma1'] * theta * self.deds[a2]) f = ((y1 * p2['eta2'] + y2 * p1['eta2']) + (y1 + y2) * self.acut * theta * x) * d / r self.forces[a1] -= f self.forces[a2] += f
class TestGeometry(unittest.TestCase): """Container for tests to the geometry module.""" def __init__(self, *args, **kwargs): super(TestGeometry, self).__init__(*args, **kwargs) def shortDescription(self): """Silences unittest from printing the docstrings in test cases.""" return None def setUp(self): """Sets up some basic stuff which can be useful in the tests.""" cutoff = 3.0 self.structure = bulk('Al') self.neighborlist = NeighborList(len(self.structure) * [cutoff / 2], skin=1e-8, bothways=True, self_interaction=False) self.neighborlist.update(self.structure) def test_get_scaled_positions(self): """ Tests the test_get_scaled_positions method. """ positions = np.array([[6.5, 5.1, 3.0], [-0.1, 1.3, 4.5], [0, 0, 0], [15, 7, 4.5]]) cell = np.array([[4.0, 1.0, 0.1], [-0.4, 6.7, 0], [-4, 2, 16]]) retval = get_scaled_positions(positions, cell, wrap=False) targetval = np.array([[1.84431247, 0.43339424, 0.17597305], [0.26176336, 0.07149383, 0.27961398], [0, 0, 0], [4.04248515, 0.36500685, 0.25598447]]) np.testing.assert_almost_equal(retval, targetval) retval = get_scaled_positions(positions, cell, wrap=True) targetval = np.array([[0.84431247, 0.43339424, 0.17597305], [0.26176336, 0.07149383, 0.27961398], [0, 0, 0], [0.04248515, 0.36500685, 0.25598447]]) np.testing.assert_almost_equal(retval, targetval) retval = get_scaled_positions(positions, cell, wrap=True, pbc=3 * [False]) targetval = np.array([[1.84431247, 0.43339424, 0.17597305], [0.26176336, 0.07149383, 0.27961398], [0, 0, 0], [4.04248515, 0.36500685, 0.25598447]]) np.testing.assert_almost_equal(retval, targetval) retval = get_scaled_positions(positions, cell, wrap=True, pbc=[True, True, False]) targetval = np.array([[0.84431247, 0.43339424, 0.17597305], [0.26176336, 0.07149383, 0.27961398], [0, 0, 0], [0.04248515, 0.36500685, 0.25598447]]) np.testing.assert_almost_equal(retval, targetval) def test_get_primitive_structure(self) -> None: """ Tests the get_primitive_structure method. """ def compare_structures(s1: Atoms, s2: Atoms) -> bool: if len(s1) != len(s2): return False if not np.all(np.isclose(s1.cell, s2.cell)): return False if not np.all( np.isclose(s1.get_scaled_positions(), s2.get_scaled_positions())): return False if not np.all( s1.get_chemical_symbols() == s2.get_chemical_symbols()): return False return True structure = bulk('Al', crystalstructure='fcc', a=4).repeat(2) retval = get_primitive_structure(structure, no_idealize=True, to_primitive=True) targetval = Atoms('Al', cell=[[0.0, 2.0, 2.0], [2.0, 0.0, 2.0], [2.0, 2.0, 0.0]], pbc=True, positions=[[0, 0, 0]]) self.assertTrue(compare_structures(retval, targetval)) structure = bulk('Al', crystalstructure='fcc', a=4).repeat(2) noise_level = 1e-3 structure[0].position += noise_level * np.array([1, -5, 3]) structure[3].position += noise_level * np.array([-2, 3, -1]) retval = get_primitive_structure(structure, no_idealize=True, to_primitive=True) targetval = Atoms(8 * 'Al', cell=[[-4.0, 0.0, -4.0], [-4.0, 4.0, 0.0], [0.0, 4.0, -4.0]], pbc=True, positions=[[-3.999e+00, 7.995e+00, -3.997e+00], [-2.000e+00, 2.000e+00, -4.000e+00], [-2.000e+00, 0.000e+00, -2.000e+00], [-2.000e-03, 2.003e+00, -2.001e+00], [-4.000e+00, 2.000e+00, -2.000e+00], [-2.000e+00, 4.000e+00, -2.000e+00], [-2.000e+00, 2.000e+00, 0.000e+00], [-4.000e+00, 4.000e+00, -4.000e+00]]) self.assertTrue(compare_structures(retval, targetval)) structure = bulk('Al', crystalstructure='fcc', a=4).repeat(2) noise_level = 1e-7 structure[0].position += noise_level * np.array([1, -5, 3]) structure[3].position += noise_level * np.array([-2, 3, -1]) retval = get_primitive_structure(structure, no_idealize=True, to_primitive=True) targetval = Atoms( 'Al', cell=[[0.0, 2.0, 2.0], [2.0, 0.0, 2.0], [2.0, 2.0, 0.0]], pbc=True, positions=[[-1.24999998e-08, -2.49999997e-08, 2.50000001e-08]]) self.assertTrue(compare_structures(retval, targetval)) structure = bulk('SiC', crystalstructure='zincblende', a=4).repeat( (2, 2, 1)) structure.pbc = [True, True, False] retval = get_primitive_structure(structure, no_idealize=True, to_primitive=True) targetval = Atoms('SiC', cell=[[0.0, 2.0, 2.0], [2.0, 0.0, 2.0], [2.0, 2.0, 0.0]], pbc=[True, True, False], positions=[[0, 0, 0], [1, 1, 1]]) self.assertTrue(compare_structures(retval, targetval)) structure = bulk('SiC', crystalstructure='zincblende', a=4).repeat( (2, 2, 1)) structure.pbc = [True, True, False] retval = get_primitive_structure(structure, no_idealize=True, to_primitive=False) targetval = Atoms(4 * 'SiC', cell=[4, 4, 4], pbc=[True, True, False], positions=[[0, 0, 0], [1, 1, 1], [0, 2, 2], [1, 3, 3], [2, 0, 2], [3, 1, 3], [2, 2, 0], [3, 3, 1]]) self.assertTrue(compare_structures(retval, targetval)) structure = bulk('SiC', crystalstructure='zincblende', a=4) structure.cell = [[0.0, 2.0, 2.0], [2.0, 0.0, 2.0], [2.0, 2.0, 0.00001]] retval = get_primitive_structure(structure, no_idealize=False, to_primitive=True, symprec=1e-3) targetval = Atoms('SiC', cell=[[0.0, 1.9999983333374998, 1.9999983333374998], [1.9999983333374998, 0.0, 1.9999983333374998], [1.9999983333374998, 1.9999983333374998, 0.0]], pbc=[True, True, False], positions=[[0, 0, 0], [0.99999917, 0.99999917, 0.99999917]]) self.assertTrue(compare_structures(retval, targetval)) # Catch failure from spglib due to large symprec symprec = 0.3 prim = bulk('Au', a=1.0, cubic=True) prim[0].position += [0, 0, 0.01] prim[1].position += [-0.01, 0, 0.01] prim[2].position += [0, 0.01, 0.01] prim[3].position += [0, 0, -0.01] prim.cell[0][0] += 0.002 with self.assertRaises(ValueError) as cm: get_primitive_structure(prim, symprec=symprec) self.assertIn('spglib failed to find the primitive cell', str(cm.exception)) def test_fractional_to_cartesian(self): """Tests the geometry function fractional_to_cartesian.""" # reference data structure = bulk('Al') frac_pos = np.array([[0.0, 0.0, -0.0], [0.0, 0.0, 1.0], [0.0, 1.0, -1.0], [0.0, 1.0, -0.0], [1.0, -1.0, -0.0], [1.0, 0.0, -1.0], [1.0, 0.0, -0.0], [0.0, 0.0, -1.0], [0.0, -1.0, 1.0], [0.0, -1.0, -0.0], [-1.0, 1.0, -0.0], [-1.0, 0.0, 1.0], [-1.0, 0.0, -0.0]]) cart_pos_target = [[0., 0., 0.], [2.025, 2.025, 0.], [0., -2.025, 2.025], [2.025, 0., 2.025], [-2.025, 2.025, 0.], [-2.025, 0., 2.025], [0., 2.025, 2.025], [-2.025, -2.025, 0.], [0., 2.025, -2.025], [-2.025, 0., -2.025], [2.025, -2.025, 0.], [2.025, 0., -2.025], [0., -2.025, -2.025]] # Transform to cartesian cart_pos_predicted = [] for fractional in frac_pos: cart_pos_predicted.append( fractional_to_cartesian(structure, fractional)) # Test if predicted cartesian positions are equal to target for target, predicted in zip(cart_pos_target, cart_pos_predicted): np.testing.assert_almost_equal(target, predicted) def test_get_permutation(self): """Tests the get_permutation function.""" value = ['a', 'b', 'c'] target = ['a', 'b', 'c'] permutation = [0, 1, 2] self.assertEqual(target, get_permutation(value, permutation)) value = ['a', 'b', 'c'] target = ['a', 'c', 'b'] permutation = [0, 2, 1] self.assertEqual(target, get_permutation(value, permutation)) value = [0, 3, 'c'] target = [3, 'c', 0] permutation = [1, 2, 0] self.assertEqual(target, get_permutation(value, permutation)) # Error on permutation list too short with self.assertRaises(Exception): get_permutation(value, [0, 1]) # Error on permutation list not unique values with self.assertRaises(Exception): get_permutation(value, [0, 1, 1]) # Error on permutation list going out of range with self.assertRaises(IndexError): get_permutation(value, [0, 1, 3]) def test_ase_atoms_to_spglib_cell(self): """ Tests that function returns the right tuple from the provided ASE Atoms object. """ structure = bulk('Al').repeat(3) structure[1].symbol = 'Ag' cell, positions, species \ = ase_atoms_to_spglib_cell(self.structure) self.assertTrue((cell == self.structure.get_cell()).all()) self.assertTrue( (positions == self.structure.get_scaled_positions()).all()) self.assertTrue((species == self.structure.get_atomic_numbers()).all()) def test_chemical_symbols_to_numbers(self): """Tests chemical_symbols_to_numbers method.""" symbols = ['Al', 'H', 'He'] expected_numbers = [13, 1, 2] retval = chemical_symbols_to_numbers(symbols) self.assertEqual(expected_numbers, retval) def test_atomic_number_to_chemical_symbol(self): """Tests chemical_symbols_to_numbers method.""" numbers = [13, 1, 2] expected_symbols = ['Al', 'H', 'He'] retval = atomic_number_to_chemical_symbol(numbers) self.assertEqual(expected_symbols, retval) def test_get_wyckoff_sites(self): """Tests get_wyckoff_sites method.""" # structures and reference data to test structures, targetvals = [], [] structures.append(bulk('Po', crystalstructure='sc', a=4)) targetvals.append(['1a']) structures.append(bulk('W', crystalstructure='bcc', a=4)) targetvals.append(['2a']) structures.append(bulk('Al', crystalstructure='fcc', a=4)) targetvals.append(['4a']) structures.append(bulk('Ti', crystalstructure='hcp', a=4, c=6)) targetvals.append(2 * ['2d']) structures.append(bulk('SiC', crystalstructure='zincblende', a=4)) targetvals.append(['4a', '4d']) structures.append(bulk('NaCl', crystalstructure='rocksalt', a=4)) targetvals.append(['4a', '4b']) structures.append(bulk('ZnO', crystalstructure='wurtzite', a=4, c=5)) targetvals.append(4 * ['2b']) structures.append(bulk('Al', crystalstructure='fcc', a=4, cubic=True)) targetvals.append(4 * ['4a']) structures.append(bulk('Al').repeat(2)) targetvals.append(8 * ['4a']) structures.append(bulk('Ti').repeat((3, 2, 1))) targetvals.append(12 * ['2d']) structure = bulk('Al').repeat(2) structure[0].position += [0, 0, 0.1] structures.append(structure) targetvals.append(['2a', '4b', '8c', '8c', '8c', '8c', '4b', '2a']) for structure, targetval in zip(structures, targetvals): retval = get_wyckoff_sites(structure) self.assertEqual(targetval, retval) structure = bulk('GaAs', crystalstructure='zincblende', a=3.0).repeat(2) structure.set_chemical_symbols([ 'Ga', 'As', 'Al', 'As', 'Ga', 'As', 'Al', 'As', 'Ga', 'As', 'Ga', 'As', 'Al', 'As', 'Ga', 'As' ]) retval = get_wyckoff_sites(structure) targetval = [ '8g', '8i', '4e', '8i', '8g', '8i', '2c', '8i', '2d', '8i', '8g', '8i', '4e', '8i', '8g', '8i' ] self.assertEqual(targetval, retval) retval = get_wyckoff_sites(structure, map_occupations=[['Ga', 'Al'], ['As']]) targetval = 8 * ['4a', '4c'] self.assertEqual(targetval, retval) retval = get_wyckoff_sites(structure, map_occupations=[]) targetval = len(structure) * ['8a'] self.assertEqual(targetval, retval)
def calculate(self, atoms=None, properties=["energy"], system_changes=all_changes): Calculator.calculate(self, atoms, properties, system_changes) image = atoms params_dict = self.params chemical_symbols = np.array(image.get_chemical_symbols()) params = [] for element in chemical_symbols: re = params_dict[element]["re"] D = params_dict[element]["De"] # sig calculated from pubs.acs.org/doi/pdf/10.1021/acs.jpca.7b11252 sig = re - np.log(2) / params_dict[element]["a"] params.append(np.array([[re, D, sig]])) params = np.vstack(np.array(params)) n = NeighborList( cutoffs=[self.cutoff / 2.0] * len(image), self_interaction=False, primitive=NewPrimitiveNeighborList, ) n.update(image) image_neighbors = [ n.get_neighbors(index) for index in range(len(image)) ] natoms = len(image) positions = image.positions cell = image.cell energy = 0.0 forces = np.zeros((natoms, 3)) for a1 in range(natoms): re_1 = params[a1][0] D_1 = np.abs(params[a1][1]) sig_1 = params[a1][2] neighbors, offsets = image_neighbors[a1] cells = np.dot(offsets, cell) d = positions[neighbors] + cells - positions[a1] re_n = params[neighbors][:, 0] D_n = params[neighbors][:, 1] sig_n = params[neighbors][:, 2] if self.combo == "mean": D = np.sqrt(D_1 * D_n) sig = (sig_1 + sig_n) / 2 re = (re_1 + re_n) / 2 elif self.combo == "yang": D = (2 * D_1 * D_n) / (D_1 + D_n) sig = (sig_1 * sig_n) * (sig_1 + sig_n) / (sig_1**2 + sig_n**2) re = (re_1 * re_n) * (re_1 + re_n) / (re_1**2 + re_n**2) r = np.sqrt((d**2).sum(1)) r_star = r / sig re_star = re / sig C = np.log(2) / (re_star - 1) atom_energy = D * (np.exp(-2 * C * (r_star - re_star)) - 2 * np.exp(-C * (r_star - re_star))) energy += atom_energy.sum() f = ((2 * D * C / sig) * (1 / r) * (np.exp(-2 * C * (r_star - re_star)) - np.exp(-C * (r_star - re_star))))[:, np.newaxis] * d forces[a1] -= f.sum(axis=0) for a2, f2 in zip(neighbors, f): forces[a2] += f2 self.results["energy"] = energy self.results["forces"] = forces
class TorchAtoms(Atoms): def __init__(self, ase_atoms=None, energy=None, forces=None, cutoff=None, descriptors=[], group=None, ranks=None, **kwargs): super().__init__(**kwargs) if ase_atoms: self.__dict__ = ase_atoms.__dict__ # ------------------------------- ---------- if type(ranks) == Distributer: self.ranks = None ranks(self) else: self.ranks = ranks if group is not None: self.attach_process_group(group) else: self.is_distributed = False self.cutoff = cutoff self.descriptors = descriptors self.changes = AtomsChanges(self) if cutoff is not None: self.build_nl(cutoff) self.update(forced=True) # ------------------------------------------ try: self.target_energy = as_tensor(energy) self.target_forces = as_tensor(forces) except RuntimeError: if ase_atoms is not None and ase_atoms.get_calculator( ) is not None: if 'energy' in ase_atoms.calc.results: self.target_energy = as_tensor( ase_atoms.get_potential_energy()) if 'forces' in ase_atoms.calc.results: self.target_forces = as_tensor(ase_atoms.get_forces()) def set_targets(self): self.target_energy = as_tensor(self.get_potential_energy()) self.target_forces = as_tensor(self.get_forces()) def attach_process_group(self, group): self.process_group = group self.is_distributed = True self.index_distribute() def index_distribute(self, randomize=True): if self.is_distributed: rank = torch.distributed.get_rank(group=self.process_group) if self.ranks: self.indices = [] for i, j in enumerate(self.ranks): if j == rank: self.indices.append(i) else: workers = torch.distributed.get_world_size( group=self.process_group) indices = balance_work(self.natoms, workers) if randomize: # reproducibility issue: rnd sequence becomes workers dependent # w = np.random.permutation(workers) w = (np.arange(workers) + np.random.randint(1024)) % workers j = np.random.permutation(self.natoms) self.indices = j[range(*indices[w[rank]])].tolist() else: self.indices = range(*indices[rank]) else: self.indices = range(self.natoms) def detach_process_group(self): del self.process_group self.is_distributed = False self.index_distribute() def build_nl(self, rc): self.nl = NeighborList(self.natoms * [rc / 2], skin=0.0, self_interaction=False, bothways=True) self.cutoff = rc self.xyz = torch.from_numpy(self.positions) try: self.lll = torch.from_numpy(self.cell) except TypeError: self.lll = torch.from_numpy(self.cell.array) # distributed setup self.index_distribute() def local(self, a, stage=True, dont_save_grads=True, detach=False): n, off = self.nl.get_neighbors(a) cells = (from_numpy(off[..., None].astype(np.float)) * self.lll).sum(dim=1) r = self.xyz[n] - self.xyz[a] + cells if detach: r = r.detach() loc = Local(a, n, self.numbers[a], self.numbers[n], r, off, self.descriptors if stage else [], dont_save_grads=dont_save_grads) loc.natoms = self.natoms return loc def update(self, cutoff=None, descriptors=None, forced=False, build_locals=True, stage=True, posgrad=False, cellgrad=False, dont_save_grads=False): if cutoff or self.changes.numbers: self.build_nl(cutoff if cutoff else self.cutoff) forced = True if descriptors: self.descriptors = descriptors forced = True if forced or self.changes.atoms: self.nl.update(self) self.xyz.requires_grad = posgrad self.lll.requires_grad = cellgrad self.loc = [ self.local(a, stage=stage, dont_save_grads=dont_save_grads) for a in self.indices ] if build_locals else None self.changes.update_references() def stage(self, descriptors=None, dont_save_grads=True): descs = iterable(descriptors) if descriptors else self.descriptors for loc in self.loc: loc.stage(descs, dont_save_grads=dont_save_grads) def set_descriptors(self, descriptors, stage=True, dont_save_grads=True): self.descriptors = [d for d in iterable(descriptors)] if stage: self.stage(dont_save_grads=dont_save_grads) def add_descriptors(self, descriptors, stage=True, dont_save_grads=True): self.descriptors = [d for d in self.descriptors] + \ [d for d in iterable(descriptors)] names = [d.name for d in self.descriptors] if len(set(names)) != len(self.descriptors): raise RuntimeError( f'two or more descriptors have the same names: {names}') if stage: self.stage(iterable(descriptors), dont_save_grads=dont_save_grads) @property def natoms(self): return self.get_global_number_of_atoms() @property def tnumbers(self): return torch.from_numpy(self.numbers) @property def numbers_set(self): return np.unique(self.numbers).tolist() @property def tpbc(self): return torch.from_numpy(self.pbc) def includes_species(self, species): return any([a in iterable(species) for a in self.numbers_set]) def cat(self, attr): """__getattr__ -> renamed to cat""" try: return torch.cat([env.__dict__[attr] for env in self.loc]) except KeyError: raise AttributeError() def __getitem__(self, k): """This is a overloads the behavior of ase.Atoms.""" return self.loc[k] def __iter__(self): """This is a overloads the behavior of ase.Atoms.""" for env in self.loc: yield env def first_of_each_atom_type(atoms): indices = [] unique = [] for i, a in enumerate(atoms.numbers): if a not in unique: unique += [a] indices += [i] return indices def __eq__(self, other): # Note: descriptors are excluded if other.__class__ == Local: return False elif self.natoms != other.natoms: return False elif (self.pbc != other.pbc).any(): return False elif not self.lll.allclose(other.lll): return False elif (self.numbers != other.numbers).any(): return False elif not self.xyz.allclose(other.xyz): return False else: return True def copy(self, update=True, group=True): new = TorchAtoms(positions=self.positions.copy(), cell=self.cell.copy(), numbers=self.numbers.copy(), pbc=self.pbc.copy(), ranks=self.ranks) if group and self.is_distributed: new.attach_process_group(self.process_group) assert new.indices == self.indices # TODO: ignore? if update: new.update(cutoff=self.cutoff, descriptors=self.descriptors) vel = self.get_velocities() if vel is not None: new.set_velocities(vel.copy()) return new def set_cell(self, *args, **kwargs): super().set_cell(*args, **kwargs) try: self.lll = torch.from_numpy(self.cell) except TypeError: self.lll = torch.from_numpy(self.cell.array) def set_positions(self, *args, **kwargs): super().set_positions(*args, **kwargs) self.xyz = torch.from_numpy(self.positions) def as_ase(self): atoms = Atoms(positions=self.positions, cell=self.cell, pbc=self.pbc, numbers=self.numbers) atoms.calc = self.calc # DONE: e, f if atoms.calc is not None: atoms.calc.atoms = atoms vel = self.get_velocities() if vel is not None: atoms.set_velocities(vel) return atoms def as_local(self): """ As the inverse of Local.as_atoms """ # positions[0] should to be [0, 0, 0] r = torch.as_tensor(self.positions[1:]) #a, b = np.broadcast_arrays(self.numbers[0], self.numbers[1:]) a, b = self.numbers[0], self.numbers[1:] _i = np.arange(self.natoms) i, j = np.broadcast_arrays(_i[0], _i[1:]) loc = Local(i, j, a, b, r) if 'target_energy' in self.__dict__: loc.target_energy = self.target_energy return loc def shake(self, beta=0.05, update=True): trans = np.random.laplace(0., beta, size=self.positions.shape) self.translate(trans) if update: self.update() def single_point(self): results = {} for q in ['energy', 'forces', 'stress', 'xx']: try: results[q] = self.calc.results[q] except KeyError: pass self.set_calculator(SinglePointCalculator(self, **results)) def detached(self, set_targets=True): results = {} for q in ['energy', 'forces', 'stress', 'xx']: try: results[q] = self.calc.results[q] except KeyError: pass new = self.copy() new.set_calculator(SinglePointCalculator(new, **results)) if set_targets: new.set_targets() return new def pickle_locals(self, folder='atoms'): mkdir_p(folder) for loc in self.loc: f = os.path.join(folder, f'loc_{loc.index}.pckl') torch.save(loc, f) def pickles(self, folder='atoms'): return [ torch.load(os.path.join(folder, f'loc_{i}.pckl')) for i in range(self.natoms) ] def gathered(self, folder='atoms'): if self.is_distributed and len(self.loc) < self.natoms: self.pickle_locals(folder=folder) dist.barrier(self.process_group) loc = self.pickles() else: loc = self.loc return loc def gather_(self, folder='atoms'): self.loc = self.gathered(folder=folder) self.detach_process_group() def distribute_(self, group): self.attach_process_group(group) self.loc = [self.loc[i] for i in self.indices] def counts(self, total=True): c = Counter() if total: for number in self.numbers.tolist(): c[number] += 1 else: for loc in self.loc: c[loc.number] += 1 return c
def surface_coordination(atoms, cutoff=None, verbose=True): ''' This function allows to extract the following data from the supplied Atoms object in the form of a dictionary: Per atom: - number of neighbouring atoms of specific chemical symbol Per atomic layer (based on tag): - average coordination number for all M-M combinations of all chemical species, e.g. for CuAu alloy slab an average number of Cu-Cu, Cu-Au, Au-Cu and Au-Au bonds with surrounding atoms i.e. Cu-Cu_neighbors_per_layer etc. - concentration of atoms per layer Parameters: atoms: Atoms object Surface slab containing tagged atomic layers e.g. using carmm.build.neb.symmetry.sort_z cutoff: list of floats or None Bond length cutoff distance in Angstrom can be set for each atom individually The list must contain exactly len(atoms) floats. If None, natural_cutoffs are used by default. verbose: boolean If True, analysed data that is contained in the dictionary will be printed as a table TODO: make table neater Returns: dict_CN, dict_surf_CN TODO: proper description of dictionary structure, writing csv files''' from ase.neighborlist import natural_cutoffs, NeighborList import numpy as np from collections import Counter from itertools import product dict_CN = {} if not cutoff: # Choose a cutoff to determine max bond distance cutoff = natural_cutoffs(atoms) if verbose: print("Default bond cutoffs selected:", set([(atoms[i].symbol, cutoff[i]) for i in range(len(atoms))])) # Create a symbols set based on all species present in the atoms symbols_set = sorted(list(set(atoms.symbols))) for l in set(atoms.get_tags()): index= [i.index for i in atoms if i.tag == l] for i in index: nl = NeighborList(cutoff, self_interaction=False, bothways=True) nl.update(atoms) indices, offsets = nl.get_neighbors(i) # avoid duplicate atomic indices indices_no_self = np.array(list(set([j for j in indices]))) cn_numbers = Counter(atoms[indices_no_self].symbols) # create a dictionary of relevant values for cn calculations dict_CN[i] = {"symbol":atoms[i].symbol, 'index':i, "layer":l} for k in symbols_set: dict_CN[i].update({k+"_neighbors":cn_numbers[k]}) # check all combinations of atomic symbols pr = product(symbols_set, repeat=2) dict_surf_CN= {} for layer in set(atoms.get_tags()): dict_surf_CN[layer] = {} dict_surf_CN[layer].update({"layer":layer}) # extract data for all combinations of neighbors and put at the end of the dictionary for p in product(symbols_set, repeat=2): M_M_cn = [dict_CN[x][p[0] + "_neighbors"] for x in dict_CN if\ dict_CN[x]["layer"] == layer and dict_CN[x]['symbol'] == p[1]] if not M_M_cn == []: M_M_avg_cn = np.average(M_M_cn) else: M_M_avg_cn = "N/A" dict_surf_CN[layer].update({p[0] + "_neighboring_w_" + p[1]: M_M_avg_cn}) # Check concentrations of atoms per layer for symbol in symbols_set: atoms_per_layer = len([x for x in dict_CN if dict_CN[x]["layer"] == layer]) if atoms_per_layer > 0: symbol_count_per_layer = len([symbol for x in dict_CN if dict_CN[x]["layer"] == layer and dict_CN[x]['symbol'] == symbol]) symbol_concentration_per_layer = symbol_count_per_layer / atoms_per_layer else: symbol_concentration_per_layer = 0 dict_surf_CN[layer].update({symbol+"_concentration_per_layer":symbol_concentration_per_layer}) # Preparation for verbose cn_layer_list_dict = [dict_surf_CN[i] for i in range(len(dict_surf_CN))] cn_layer_dict_keys = [dict_surf_CN[i].keys() for i in range(len(dict_surf_CN))][0] # Optional text output if verbose: print([key for key in cn_layer_dict_keys]) for i in range(len(dict_surf_CN)): print([cn_layer_list_dict[i][key] for key in cn_layer_dict_keys]) return dict_CN, dict_surf_CN
class EAM(Calculator): r""" EAM Interface Documentation Introduction ============ The Embedded Atom Method (EAM) [1]_ is a classical potential which is good for modelling metals, particularly fcc materials. Because it is an equiaxial potential the EAM does not model directional bonds well. However, the Angular Dependent Potential (ADP) [2]_ which is an extended version of EAM is able to model directional bonds and is also included in the EAM calculator. Generally all that is required to use this calculator is to supply a potential file or as a set of functions that describe the potential. The files containing the potentials for this calculator are not included but many suitable potentials can be downloaded from The Interatomic Potentials Repository Project at https://www.ctcms.nist.gov/potentials/ Theory ====== A single element EAM potential is defined by three functions: the embedded energy, electron density and the pair potential. A two element alloy contains the individual three functions for each element plus cross pair interactions. The ADP potential has two additional sets of data to define the dipole and quadrupole directional terms for each alloy and their cross interactions. The total energy `E_{\rm tot}` of an arbitrary arrangement of atoms is given by the EAM potential as .. math:: E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij}) and .. math:: \bar\rho_i = \sum_j \rho(r_{ij}) where `F` is an embedding function, namely the energy to embed an atom `i` in the combined electron density `\bar\rho_i` which is contributed from each of its neighbouring atoms `j` by an amount `\rho(r_{ij})`, `\phi(r_{ij})` is the pair potential function representing the energy in bond `ij` which is due to the short-range electro-static interaction between atoms, and `r_{ij}` is the distance between an atom and its neighbour for that bond. The ADP potential is defined as .. math:: E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij}) + \frac{1}{2} \sum_{i,\alpha} (\mu_i^\alpha)^2 + \frac{1}{2} \sum_{i,\alpha,\beta} (\lambda_i^{\alpha\beta})^2 - \frac{1}{6} \sum_i \nu_i^2 where `\mu_i^\alpha` is the dipole vector, `\lambda_i^{\alpha\beta}` is the quadrupole tensor and `\nu_i` is the trace of `\lambda_i^{\alpha\beta}`. The fs potential is defined as .. math:: E_i = F_\alpha (\sum_{j\neq i} \rho_{\alpha \beta}(r_{ij})) + \frac{1}{2}\sum_{j\neq i}\phi_{\alpha \beta}(r_{ij}) where `\alpha` and `\beta` are element types of atoms. This form is similar to original EAM formula above, except that `\rho` and `\phi` are determined by element types. Running the Calculator ====================== EAM calculates the cohesive atom energy and forces. Internally the potential functions are defined by splines which may be directly supplied or created by reading the spline points from a data file from which a spline function is created. The LAMMPS compatible ``.alloy``, ``.fs`` and ``.adp`` formats are supported. The LAMMPS ``.eam`` format is slightly different from the ``.alloy`` format and is currently not supported. For example:: from ase.calculators.eam import EAM mishin = EAM(potential='Al99.eam.alloy') mishin.write_potential('new.eam.alloy') mishin.plot() slab.set_calculator(mishin) slab.get_potential_energy() slab.get_forces() The breakdown of energy contribution from the indvidual components are stored in the calculator instance ``.results['energy_components']`` Arguments ========= ========================= ==================================================== Keyword Description ========================= ==================================================== ``potential`` file of potential in ``.eam``, ``.alloy``, ``.adp`` or ``.fs`` format or file object (This is generally all you need to supply). In case of file object the ``form`` argument is required ``elements[N]`` array of N element abbreviations ``embedded_energy[N]`` arrays of embedded energy functions ``electron_density[N]`` arrays of electron density functions ``phi[N,N]`` arrays of pair potential functions ``d_embedded_energy[N]`` arrays of derivative embedded energy functions ``d_electron_density[N]`` arrays of derivative electron density functions ``d_phi[N,N]`` arrays of derivative pair potentials functions ``d[N,N], q[N,N]`` ADP dipole and quadrupole function ``d_d[N,N], d_q[N,N]`` ADP dipole and quadrupole derivative functions ``skin`` skin distance passed to NeighborList(). If no atom has moved more than the skin-distance since the last call to the ``update()`` method then the neighbor list can be reused. Defaults to 1.0. ``form`` the form of the potential ``eam``, ``alloy``, ``adp`` or ``fs``. This will be determined from the file suffix or must be set if using equations or file object ========================= ==================================================== Additional parameters for writing potential files ================================================= The following parameters are only required for writing a potential in ``.alloy``, ``.adp`` or ``fs`` format file. ========================= ==================================================== Keyword Description ========================= ==================================================== ``header`` Three line text header. Default is standard message. ``Z[N]`` Array of atomic number of each element ``mass[N]`` Atomic mass of each element ``a[N]`` Array of lattice parameters for each element ``lattice[N]`` Lattice type ``nrho`` No. of rho samples along embedded energy curve ``drho`` Increment for sampling density ``nr`` No. of radial points along density and pair potential curves ``dr`` Increment for sampling radius ========================= ==================================================== Special features ================ ``.plot()`` Plots the individual functions. This may be called from multiple EAM potentials to compare the shape of the individual curves. This function requires the installation of the Matplotlib libraries. Notes/Issues ============= * Although currently not fast, this calculator can be good for trying small calculations or for creating new potentials by matching baseline data such as from DFT results. The format for these potentials is compatible with LAMMPS_ and so can be used either directly by LAMMPS or with the ASE LAMMPS calculator interface. * Supported formats are the LAMMPS_ ``.alloy`` and ``.adp``. The ``.eam`` format is currently not supported. The form of the potential will be determined from the file suffix. * Any supplied values will override values read from the file. * The derivative functions, if supplied, are only used to calculate forces. * There is a bug in early versions of scipy that will cause eam.py to crash when trying to evaluate splines of a potential with one neighbor such as caused by evaluating a dimer. .. _LAMMPS: http://lammps.sandia.gov/ .. [1] M.S. Daw and M.I. Baskes, Phys. Rev. Letters 50 (1983) 1285. .. [2] Y. Mishin, M.J. Mehl, and D.A. Papaconstantopoulos, Acta Materialia 53 2005 4029--4041. End EAM Interface Documentation """ implemented_properties = ['energy', 'forces'] default_parameters = dict(skin=1.0, potential=None, header=[ b'EAM/ADP potential file\n', b'Generated from eam.py\n', b'blank\n' ]) def __init__(self, restart=None, ignore_bad_restart_file=False, label=os.curdir, atoms=None, form=None, **kwargs): self.form = form if 'potential' in kwargs: self.read_potential(kwargs['potential']) Calculator.__init__(self, restart, ignore_bad_restart_file, label, atoms, **kwargs) valid_args = ( 'potential', 'elements', 'header', 'drho', 'dr', 'cutoff', 'atomic_number', 'mass', 'a', 'lattice', 'embedded_energy', 'electron_density', 'phi', # derivatives 'd_embedded_energy', 'd_electron_density', 'd_phi', 'd', 'q', 'd_d', 'd_q', # adp terms 'skin', 'Z', 'nr', 'nrho', 'mass') # set any additional keyword arguments for arg, val in self.parameters.items(): if arg in valid_args: setattr(self, arg, val) else: raise RuntimeError('unknown keyword arg "%s" : not in %s' % (arg, valid_args)) def set_form(self, fileobj): """set the form variable based on the file name suffix""" extension = os.path.splitext(fileobj)[1] if extension == '.eam': self.form = 'eam' elif extension == '.alloy': self.form = 'alloy' elif extension == '.adp': self.form = 'adp' elif extension == '.fs': self.form = 'fs' else: raise RuntimeError('unknown file extension type: %s' % extension) def read_potential(self, fileobj): """Reads a LAMMPS EAM file in alloy or adp format and creates the interpolation functions from the data """ if isinstance(fileobj, basestring): f = open(fileobj) if self.form is None: self.set_form(fileobj) else: f = fileobj def lines_to_list(lines): """Make the data one long line so as not to care how its formatted """ data = [] for line in lines: data.extend(line.split()) return data lines = f.readlines() if self.form == 'eam': # single element eam file (aka funcfl) self.header = lines[:1] data = lines_to_list(lines[1:]) # eam form is just like an alloy form for one element self.Nelements = 1 self.Z = np.array([data[0]], dtype=int) self.mass = np.array([data[1]]) self.a = np.array([data[2]]) self.lattice = [data[3]] self.nrho = int(data[4]) self.drho = float(data[5]) self.nr = int(data[6]) self.dr = float(data[7]) self.cutoff = float(data[8]) n = 9 + self.nrho self.embedded_data = np.array([np.float_(data[9:n])]) self.rphi_data = np.zeros( [self.Nelements, self.Nelements, self.nr]) effective_charge = np.float_(data[n:n + self.nr]) # convert effective charges to rphi according to # http://lammps.sandia.gov/doc/pair_eam.html self.rphi_data[0, 0] = Bohr * Hartree * (effective_charge**2) self.density_data = np.array( [np.float_(data[n + self.nr:n + 2 * self.nr])]) elif self.form in ['alloy', 'adq']: self.header = lines[:3] i = 3 data = lines_to_list(lines[i:]) self.Nelements = int(data[0]) d = 1 self.elements = data[d:d + self.Nelements] d += self.Nelements self.nrho = int(data[d]) self.drho = float(data[d + 1]) self.nr = int(data[d + 2]) self.dr = float(data[d + 3]) self.cutoff = float(data[d + 4]) self.embedded_data = np.zeros([self.Nelements, self.nrho]) self.density_data = np.zeros([self.Nelements, self.nr]) self.Z = np.zeros([self.Nelements], dtype=int) self.mass = np.zeros([self.Nelements]) self.a = np.zeros([self.Nelements]) self.lattice = [] d += 5 # reads in the part of the eam file for each element for elem in range(self.Nelements): self.Z[elem] = int(data[d]) self.mass[elem] = float(data[d + 1]) self.a[elem] = float(data[d + 2]) self.lattice.append(data[d + 3]) d += 4 self.embedded_data[elem] = np.float_(data[d:(d + self.nrho)]) d += self.nrho self.density_data[elem] = np.float_(data[d:(d + self.nr)]) d += self.nr # reads in the r*phi data for each interaction between elements self.rphi_data = np.zeros( [self.Nelements, self.Nelements, self.nr]) for i in range(self.Nelements): for j in range(i + 1): self.rphi_data[j, i] = np.float_(data[d:(d + self.nr)]) d += self.nr elif self.form == 'fs': self.header = lines[:3] i = 3 data = lines_to_list(lines[i:]) self.Nelements = int(data[0]) d = 1 self.elements = data[d:d + self.Nelements] d += self.Nelements self.nrho = int(data[d]) self.drho = float(data[d + 1]) self.nr = int(data[d + 2]) self.dr = float(data[d + 3]) self.cutoff = float(data[d + 4]) self.embedded_data = np.zeros([self.Nelements, self.nrho]) self.density_data = np.zeros( [self.Nelements, self.Nelements, self.nr]) self.Z = np.zeros([self.Nelements], dtype=int) self.mass = np.zeros([self.Nelements]) self.a = np.zeros([self.Nelements]) self.lattice = [] d += 5 # reads in the part of the eam file for each element for elem in range(self.Nelements): self.Z[elem] = int(data[d]) self.mass[elem] = float(data[d + 1]) self.a[elem] = float(data[d + 2]) self.lattice.append(data[d + 3]) d += 4 self.embedded_data[elem] = np.float_(data[d:(d + self.nrho)]) d += self.nrho self.density_data[elem, :, :] = np.float_( data[d:(d + self.nr * self.Nelements)]).reshape( [self.Nelements, self.nr]) d += self.nr * self.Nelements # reads in the r*phi data for each interaction between elements self.rphi_data = np.zeros( [self.Nelements, self.Nelements, self.nr]) for i in range(self.Nelements): for j in range(i + 1): self.rphi_data[j, i] = np.float_(data[d:(d + self.nr)]) d += self.nr self.r = np.arange(0, self.nr) * self.dr self.rho = np.arange(0, self.nrho) * self.drho # choose the set_splines method according to the type if self.form == 'fs': self.set_fs_splines() else: self.set_splines() if (self.form == 'adp'): self.read_adp_data(data, d) self.set_adp_splines() def set_splines(self): # this section turns the file data into three functions (and # derivative functions) that define the potential self.embedded_energy = np.empty(self.Nelements, object) self.electron_density = np.empty(self.Nelements, object) self.d_embedded_energy = np.empty(self.Nelements, object) self.d_electron_density = np.empty(self.Nelements, object) for i in range(self.Nelements): self.embedded_energy[i] = spline(self.rho, self.embedded_data[i], k=3) self.electron_density[i] = spline(self.r, self.density_data[i], k=3) self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i]) self.d_electron_density[i] = self.deriv(self.electron_density[i]) self.phi = np.empty([self.Nelements, self.Nelements], object) self.d_phi = np.empty([self.Nelements, self.Nelements], object) # ignore the first point of the phi data because it is forced # to go through zero due to the r*phi format in alloy and adp for i in range(self.Nelements): for j in range(i, self.Nelements): self.phi[i, j] = spline(self.r[1:], self.rphi_data[i, j][1:] / self.r[1:], k=3) self.d_phi[i, j] = self.deriv(self.phi[i, j]) if j != i: self.phi[j, i] = self.phi[i, j] self.d_phi[j, i] = self.d_phi[i, j] def set_fs_splines(self): self.embedded_energy = np.empty(self.Nelements, object) self.electron_density = np.empty([self.Nelements, self.Nelements], object) self.d_embedded_energy = np.empty(self.Nelements, object) self.d_electron_density = np.empty([self.Nelements, self.Nelements], object) for i in range(self.Nelements): self.embedded_energy[i] = spline(self.rho, self.embedded_data[i], k=3) self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i]) for j in range(self.Nelements): self.electron_density[i, j] = spline(self.r, self.density_data[i, j], k=3) self.d_electron_density[i, j] = self.deriv( self.electron_density[i, j]) self.phi = np.empty([self.Nelements, self.Nelements], object) self.d_phi = np.empty([self.Nelements, self.Nelements], object) for i in range(self.Nelements): for j in range(i, self.Nelements): self.phi[i, j] = spline(self.r[1:], self.rphi_data[i, j][1:] / self.r[1:], k=3) self.d_phi[i, j] = self.deriv(self.phi[i, j]) if j != i: self.phi[j, i] = self.phi[i, j] self.d_phi[j, i] = self.d_phi[i, j] def set_adp_splines(self): self.d = np.empty([self.Nelements, self.Nelements], object) self.d_d = np.empty([self.Nelements, self.Nelements], object) self.q = np.empty([self.Nelements, self.Nelements], object) self.d_q = np.empty([self.Nelements, self.Nelements], object) for i in range(self.Nelements): for j in range(i, self.Nelements): self.d[i, j] = spline(self.r[1:], self.d_data[i, j][1:], k=3) self.d_d[i, j] = self.deriv(self.d[i, j]) self.q[i, j] = spline(self.r[1:], self.q_data[i, j][1:], k=3) self.d_q[i, j] = self.deriv(self.q[i, j]) # make symmetrical if j != i: self.d[j, i] = self.d[i, j] self.d_d[j, i] = self.d_d[i, j] self.q[j, i] = self.q[i, j] self.d_q[j, i] = self.d_q[i, j] def read_adp_data(self, data, d): """read in the extra adp data from the potential file""" self.d_data = np.zeros([self.Nelements, self.Nelements, self.nr]) # should be non symmetrical combinations of 2 for i in range(self.Nelements): for j in range(i + 1): self.d_data[j, i] = data[d:d + self.nr] d += self.nr self.q_data = np.zeros([self.Nelements, self.Nelements, self.nr]) # should be non symmetrical combinations of 2 for i in range(self.Nelements): for j in range(i + 1): self.q_data[j, i] = data[d:d + self.nr] d += self.nr def write_potential(self, filename, nc=1, numformat='%.8e'): """Writes out the potential in the format given by the form variable to 'filename' with a data format that is nc columns wide. Note: array lengths need to be an exact multiple of nc """ f = open(filename, 'wb') assert self.nr % nc == 0 assert self.nrho % nc == 0 for line in self.header: f.write(line) f.write('{0} '.format(self.Nelements).encode()) f.write(' '.join(self.elements).encode() + b'\n') f.write( ('%d %f %d %f %f \n' % (self.nrho, self.drho, self.nr, self.dr, self.cutoff)).encode()) # start of each section for each element # rs = np.linspace(0, self.nr * self.dr, self.nr) # rhos = np.linspace(0, self.nrho * self.drho, self.nrho) rs = np.arange(0, self.nr) * self.dr rhos = np.arange(0, self.nrho) * self.drho for i in range(self.Nelements): f.write(('%d %f %f %s\n' % (self.Z[i], self.mass[i], self.a[i], str(self.lattice[i]))).encode()) np.savetxt(f, self.embedded_energy[i](rhos).reshape( self.nrho // nc, nc), fmt=nc * [numformat]) if self.form == 'fs': for j in range(self.Nelements): np.savetxt(f, self.electron_density[i, j](rs).reshape( self.nr // nc, nc), fmt=nc * [numformat]) else: np.savetxt(f, self.electron_density[i](rs).reshape( self.nr // nc, nc), fmt=nc * [numformat]) # write out the pair potentials in Lammps DYNAMO setfl format # as r*phi for alloy format for i in range(self.Nelements): for j in range(i, self.Nelements): np.savetxt(f, (rs * self.phi[i, j](rs)).reshape( self.nr // nc, nc), fmt=nc * [numformat]) if self.form == 'adp': # these are the u(r) or dipole values for i in range(self.Nelements): for j in range(i + 1): np.savetxt(f, self.d_data[i, j]) # these are the w(r) or quadrupole values for i in range(self.Nelements): for j in range(i + 1): np.savetxt(f, self.q_data[i, j]) f.close() def update(self, atoms): # check all the elements are available in the potential self.Nelements = len(self.elements) elements = np.unique(atoms.get_chemical_symbols()) unavailable = np.logical_not( np.array([item in self.elements for item in elements])) if np.any(unavailable): raise RuntimeError('These elements are not in the potential: %s' % elements[unavailable]) # cutoffs need to be a vector for NeighborList cutoffs = self.cutoff * np.ones(len(atoms)) # convert the elements to an index of the position # in the eam format self.index = np.array( [self.elements.index(el) for el in atoms.get_chemical_symbols()]) self.pbc = atoms.get_pbc() # since we need the contribution of all neighbors to the # local electron density we cannot just calculate and use # one way neighbors self.neighbors = NeighborList(cutoffs, skin=self.parameters.skin, self_interaction=False, bothways=True) self.neighbors.update(atoms) def calculate(self, atoms=None, properties=['energy'], system_changes=all_changes): """EAM Calculator atoms: Atoms object Contains positions, unit-cell, ... properties: list of str List of what needs to be calculated. Can be any combination of 'energy', 'forces' system_changes: list of str List of what has changed since last calculation. Can be any combination of these five: 'positions', 'numbers', 'cell', 'pbc', 'initial_charges' and 'initial_magmoms'. """ Calculator.calculate(self, atoms, properties, system_changes) # we shouldn't really recalc if charges or magmos change if len(system_changes) > 0: # something wrong with this way self.update(self.atoms) self.calculate_energy(self.atoms) if 'forces' in properties: self.calculate_forces(self.atoms) # check we have all the properties requested for property in properties: if property not in self.results: if property == 'energy': self.calculate_energy(self.atoms) if property == 'forces': self.calculate_forces(self.atoms) # we need to remember the previous state of parameters # if 'potential' in parameter_changes and potential != None: # self.read_potential(potential) def calculate_energy(self, atoms): """Calculate the energy the energy is made up of the ionic or pair interaction and the embedding energy of each atom into the electron cloud generated by its neighbors """ pair_energy = 0.0 embedding_energy = 0.0 mu_energy = 0.0 lam_energy = 0.0 trace_energy = 0.0 self.total_density = np.zeros(len(atoms)) if (self.form == 'adp'): self.mu = np.zeros([len(atoms), 3]) self.lam = np.zeros([len(atoms), 3, 3]) for i in range(len(atoms)): # this is the atom to be embedded neighbors, offsets = self.neighbors.get_neighbors(i) offset = np.dot(offsets, atoms.get_cell()) rvec = (atoms.positions[neighbors] + offset - atoms.positions[i]) # calculate the distance to the nearest neighbors r = np.sqrt(np.sum(np.square(rvec), axis=1)) # fast # r = np.apply_along_axis(np.linalg.norm, 1, rvec) # sloow nearest = np.arange(len(r))[r <= self.cutoff] for j_index in range(self.Nelements): use = self.index[neighbors[nearest]] == j_index if not use.any(): continue pair_energy += np.sum(self.phi[self.index[i], j_index]( r[nearest][use])) / 2. if self.form == 'fs': density = np.sum( self.electron_density[j_index, self.index[i]](r[nearest][use])) else: density = np.sum(self.electron_density[j_index]( r[nearest][use])) self.total_density[i] += density if self.form == 'adp': self.mu[i] += self.adp_dipole( r[nearest][use], rvec[nearest][use], self.d[self.index[i], j_index]) self.lam[i] += self.adp_quadrupole( r[nearest][use], rvec[nearest][use], self.q[self.index[i], j_index]) # add in the electron embedding energy embedding_energy += self.embedded_energy[self.index[i]]( self.total_density[i]) components = dict(pair=pair_energy, embedding=embedding_energy) if self.form == 'adp': mu_energy += np.sum(self.mu**2) / 2. lam_energy += np.sum(self.lam**2) / 2. for i in range(len(atoms)): # this is the atom to be embedded trace_energy -= np.sum(self.lam[i].trace()**2) / 6. adp_result = dict(adp_mu=mu_energy, adp_lam=lam_energy, adp_trace=trace_energy) components.update(adp_result) self.positions = atoms.positions.copy() self.cell = atoms.get_cell().copy() energy = 0.0 for i in components.keys(): energy += components[i] self.energy_free = energy self.energy_zero = energy self.results['energy_components'] = components self.results['energy'] = energy def calculate_forces(self, atoms): # calculate the forces based on derivatives of the three EAM functions self.update(atoms) self.results['forces'] = np.zeros((len(atoms), 3)) for i in range(len(atoms)): # this is the atom to be embedded neighbors, offsets = self.neighbors.get_neighbors(i) offset = np.dot(offsets, atoms.get_cell()) # create a vector of relative positions of neighbors rvec = atoms.positions[neighbors] + offset - atoms.positions[i] r = np.sqrt(np.sum(np.square(rvec), axis=1)) nearest = np.arange(len(r))[r < self.cutoff] d_embedded_energy_i = self.d_embedded_energy[self.index[i]]( self.total_density[i]) urvec = rvec.copy() # unit directional vector for j in np.arange(len(neighbors)): urvec[j] = urvec[j] / r[j] for j_index in range(self.Nelements): use = self.index[neighbors[nearest]] == j_index if not use.any(): continue rnuse = r[nearest][use] density_j = self.total_density[neighbors[nearest][use]] if self.form == 'fs': scale = (self.d_phi[self.index[i], j_index](rnuse) + (d_embedded_energy_i * self.d_electron_density[j_index, self.index[i]] (rnuse)) + (self.d_embedded_energy[j_index](density_j) * self.d_electron_density[self.index[i], j_index] (rnuse))) else: scale = (self.d_phi[self.index[i], j_index](rnuse) + (d_embedded_energy_i * self.d_electron_density[j_index](rnuse)) + (self.d_embedded_energy[j_index](density_j) * self.d_electron_density[self.index[i]](rnuse))) self.results['forces'][i] += np.dot(scale, urvec[nearest][use]) if (self.form == 'adp'): adp_forces = self.angular_forces( self.mu[i], self.mu[neighbors[nearest][use]], self.lam[i], self.lam[neighbors[nearest][use]], rnuse, rvec[nearest][use], self.index[i], j_index) self.results['forces'][i] += adp_forces def angular_forces(self, mu_i, mu, lam_i, lam, r, rvec, form1, form2): # calculate the extra components for the adp forces # rvec are the relative positions to atom i psi = np.zeros(mu.shape) for gamma in range(3): term1 = (mu_i[gamma] - mu[:, gamma]) * self.d[form1][form2](r) term2 = np.sum( (mu_i - mu) * self.d_d[form1][form2](r)[:, np.newaxis] * (rvec * rvec[:, gamma][:, np.newaxis] / r[:, np.newaxis]), axis=1) term3 = 2 * np.sum((lam_i[:, gamma] + lam[:, :, gamma]) * rvec * self.q[form1][form2](r)[:, np.newaxis], axis=1) term4 = 0.0 for alpha in range(3): for beta in range(3): rs = rvec[:, alpha] * rvec[:, beta] * rvec[:, gamma] term4 += ((lam_i[alpha, beta] + lam[:, alpha, beta]) * self.d_q[form1][form2](r) * rs) / r term5 = ((lam_i.trace() + lam.trace(axis1=1, axis2=2)) * (self.d_q[form1][form2](r) * r + 2 * self.q[form1][form2] (r)) * rvec[:, gamma]) / 3. # the minus for term5 is a correction on the adp # formulation given in the 2005 Mishin Paper and is posted # on the NIST website with the AlH potential psi[:, gamma] = term1 + term2 + term3 + term4 - term5 return np.sum(psi, axis=0) def adp_dipole(self, r, rvec, d): # calculate the dipole contribution mu = np.sum((rvec * d(r)[:, np.newaxis]), axis=0) return mu # sign to agree with lammps def adp_quadrupole(self, r, rvec, q): # slow way of calculating the quadrupole contribution r = np.sqrt(np.sum(rvec**2, axis=1)) lam = np.zeros([rvec.shape[0], 3, 3]) qr = q(r) for alpha in range(3): for beta in range(3): lam[:, alpha, beta] += qr * rvec[:, alpha] * rvec[:, beta] return np.sum(lam, axis=0) def deriv(self, spline): """Wrapper for extracting the derivative from a spline""" def d_spline(aspline): return spline(aspline, 1) return d_spline def plot(self, name=''): """Plot the individual curves""" import matplotlib.pyplot as plt if self.form == 'eam' or self.form == 'alloy' or self.form == 'fs': nrow = 2 elif self.form == 'adp': nrow = 3 else: raise RuntimeError('Unknown form of potential: %s' % self.form) if hasattr(self, 'r'): r = self.r else: r = np.linspace(0, self.cutoff, 50) if hasattr(self, 'rho'): rho = self.rho else: rho = np.linspace(0, 10.0, 50) plt.subplot(nrow, 2, 1) self.elem_subplot(rho, self.embedded_energy, r'$\rho$', r'Embedding Energy $F(\bar\rho)$', name, plt) plt.subplot(nrow, 2, 2) if self.form == 'fs': self.multielem_subplot(r, self.electron_density, r'$r$', r'Electron Density $\rho(r)$', name, plt, half=False) else: self.elem_subplot(r, self.electron_density, r'$r$', r'Electron Density $\rho(r)$', name, plt) plt.subplot(nrow, 2, 3) self.multielem_subplot(r, self.phi, r'$r$', r'Pair Potential $\phi(r)$', name, plt) plt.ylim(-1.0, 1.0) # need reasonable values if self.form == 'adp': plt.subplot(nrow, 2, 5) self.multielem_subplot(r, self.d, r'$r$', r'Dipole Energy', name, plt) plt.subplot(nrow, 2, 6) self.multielem_subplot(r, self.q, r'$r$', r'Quadrupole Energy', name, plt) plt.plot() def elem_subplot(self, curvex, curvey, xlabel, ylabel, name, plt): plt.xlabel(xlabel) plt.ylabel(ylabel) for i in np.arange(self.Nelements): label = name + ' ' + self.elements[i] plt.plot(curvex, curvey[i](curvex), label=label) plt.legend() def multielem_subplot(self, curvex, curvey, xlabel, ylabel, name, plt, half=True): plt.xlabel(xlabel) plt.ylabel(ylabel) for i in np.arange(self.Nelements): for j in np.arange((i + 1) if half else self.Nelements): label = name + ' ' + self.elements[i] + '-' + self.elements[j] plt.plot(curvex, curvey[i, j](curvex), label=label) plt.legend()
for i in glob('*/'): if i == '__pycache__/': continue dire = i atoms = read(i + "/" + "POSCAR") ################# These can be added in as arg parsers ################## ads = read("CO.POSCAR") surface_atoms = ["Pt"] radii_multiplier = 1.1 skin_arg = 0.25 no_adsorb = [''] min_ads_dist = 2.4 nl = NeighborList(natural_cutoffs(atoms, radii_multiplier), self_interaction=False, bothways=True, skin=skin_arg) nl.update(atoms) adsorbate_atoms = [ index for index, atom in enumerate(atoms) if atom.symbol not in surface_atoms ] normals, mask = generate_normals(atoms, adsorbate_atoms=adsorbate_atoms, normalize_final=True) ### make sure to manually set the normals for 2-D materials, all atoms should have a normal pointing up, as all atoms are surface atoms #normals, mask = np.ones((len(atoms), 3)) * (0, 0, 1), list(range(len(atoms))) constrained = constrained_indices(atoms)
class SHPP(Calculator): """ Soft+Harmonic Pair Potential -- a combination of LAMMPS-pair_style 'soft' pair potential plus restorative forces. The total energy is given by: E_tot(R) = E_harm(R) + E_soft(R) E_harm(R) = Sum_i 0.5*k*|R_i-R_i,start|^2 E_soft(R) = Sum_i<j A*(1+cos(|R_i-R_j|*pi/d_min)) atoms: an Atoms object blmin: dictionary with the minimal interatomic distance for each (sorted) pair of atomic numbers k: spring constant for the harmonic potential in eV/Angstrom^2 A: prefactor for the 'soft' potential in eV """ implemented_properties = ['energy', 'forces'] def __init__(self, atoms, blmin, k=0.5, A=10.): Calculator.__init__(self) self.positions = atoms.get_positions() self.blmin = blmin self.k = k self.A = A self.N = len(atoms) rcut = max(self.blmin.values()) self.nl = NeighborList([rcut / 2.] * self.N, skin=1., bothways=True, self_interaction=False) def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) energy, forces = 0, np.zeros((self.N, 3)) e, f = self._calculate_harm(atoms) energy += e forces += f e, f = self._calculate_soft(atoms) energy += e forces += f self.results = {'energy': energy, 'forces': forces} def _calculate_harm(self, atoms): cell = atoms.get_cell() pbc = atoms.get_pbc() vectors = atoms.get_positions() - self.positions e = 0. f = np.zeros((self.N, 3)) for i, v in enumerate(vectors): v, d = find_mic([v], cell, pbc) e += 0.5 * self.k * (d**2) f[i] = -self.k * v return e, f def _calculate_soft(self, atoms): cell = atoms.get_cell() num = atoms.get_atomic_numbers() pos = atoms.get_positions() self.nl.update(atoms) e = 0 f = np.zeros((self.N, 3)) for i in range(self.N): indices, offsets = self.nl.get_neighbors(i) p = pos[indices] + np.dot(offsets, cell) r = cdist(p, [pos[i]]) v = p - pos[i] for j, index in enumerate(indices): bl = self.blmin[tuple(sorted([num[i], num[index]]))] d = r[j][0] if d < bl: e += self.A * (1 + np.cos(d * np.pi / bl)) fj = self.A * np.pi * np.sin(np.pi * d / bl) fj /= (d * bl) * v[j] f[index] += fj return e, f
def load_oqmd_data(num_dist_basis, dtype=float, cutoff=2.0, filter_query=None): """ Returns tuple (Z, D, y, num_species) where Z is a matrix, each row in Z corresponds to a molecule and the elements are atomic numbers D is a 4D tensor giving the Gaussian feature expansion of distances between atoms y is the energy of each molecule in kcal/mol num_species is the integer number of different atomic species in Z """ import ase.db con = ase.db.connect("oqmd_all_entries.db") sel = filter_query # Create sorted list of species all_species = sorted( list( reduce(lambda x, y: x.union(set(y.numbers)), con.select(sel), set()))) # Insert dummy species 0 all_species.insert(0, 0) # Create mapping from species to species index species_map = dict([(x, i) for i, x in enumerate(all_species)]) # Find max number of atoms max_atoms = 0 num_molecules = 0 for row in con.select(sel): num_molecules += 1 atoms = row.toatoms() max_atoms = max(max_atoms, len(atoms)) # Gaussian basis function parameters mu_min = -1 mu_max = 2 * cutoff + 1 step = (mu_max - mu_min) / num_dist_basis sigma = step print "Number of molecules %d, max atoms %d" % (num_molecules, max_atoms) k = np.arange(num_dist_basis) D = np.zeros((num_molecules, max_atoms, max_atoms, num_dist_basis), dtype=dtype) Z = np.zeros((num_molecules, max_atoms), dtype=np.int8) y = np.zeros(num_molecules, dtype=dtype) for i_mol, row in enumerate(con.select(sel)): print "feature expansion %10d/%10d\r" % (i_mol + 1, num_molecules), atoms = row.toatoms() y[i_mol] = row.total_energy Z[i_mol, 0:len(row.numbers)] = map(lambda x: species_map[x], row.numbers) assert np.all(atoms.get_atomic_numbers() == row.numbers) neighborhood = NeighborList([cutoff] * len(atoms), self_interaction=False, bothways=False) neighborhood.build(atoms) for ii in range(len(atoms)): neighbor_indices, offset = neighborhood.get_neighbors(ii) for jj, offs in zip(neighbor_indices, offset): ii_pos = atoms.positions[ii] jj_pos = atoms.positions[jj] + np.dot(offs, atoms.get_cell()) dist = np.linalg.norm(ii_pos - jj_pos) d_expand = np.exp(-((dist - (mu_min + k * step))**2.0) / (2.0 * sigma**2.0)) D[i_mol, ii, jj, :] += d_expand if jj != ii: D[i_mol, jj, ii, :] += d_expand return Z, D, y, len(all_species)
def generate_site_type(atoms, surface_mask, normals, coordination, unallowed_elements=[]): cutoffs = natural_cutoffs(atoms) nl = NeighborList(cutoffs, self_interaction=False, bothways=True) nl.update(atoms) surface_mask = [index for index in surface_mask if atoms[index].symbol not in unallowed_elements] possible = list(combinations(set(surface_mask), coordination)) valid = [] sites = [] for cycle in possible: for start, end in combinations(cycle, 2): if end not in nl.get_neighbors(start)[0]: break else: # All were valid valid.append(list(cycle)) #print(valid) for cycle in valid: tracked = np.array(atoms[cycle[0]].position, dtype=float) known = np.zeros(shape=(coordination, 3), dtype=float) known[0] = tracked for index, (start, end) in enumerate(zip(cycle[:-1], cycle[1:])): for neighbor, offset in zip(*nl.get_neighbors(start)): if neighbor == end: tracked += offset_position(atoms, neighbor, offset) - atoms[start].position known[index + 1] = tracked average = np.average(known, axis=0) normal = np.zeros(3) #for index in cycle: #neighbors = len(nl.get_neighbors(index)[0]) #normal += normals[index] * (1/neighbors) #normal = normalize(normal) #for index in cycle: #print(cycle) if len(cycle) == 1: for index in cycle: neighbors = len(nl.get_neighbors(index)[0]) normal += normals[index] * (1/neighbors) normal = normalize(normal) if len(cycle) >1: neighbors = len(nl.get_neighbors(index)[0]) cycle_orig = cycle #print(cycle) normal = generate_normals_new(atoms,cycle_orig,nl,surface_mask) #print(cycle,normal) for index in cycle: neighbors = len(nl.get_neighbors(index)[0]) normal += normals[index] * (1/neighbors) normal = normalize(normal) if coordination ==2: average[2] = average[2] - 0.5 if coordination == 3: average[2] = average[2] -0.7 #print(average) #print(average[2]) site_ads =Site(cycle=cycle, position=average, normal=normal) sites.append(site_ads) return sites
def CoreShellCN(atoms, type_a, type_b, ratio, R_min = 1.5, CN_max=12, n_depth=-1): r"""This routine generates cluster with ideal core-shell architecture, so that atoms of type_a are placed on the surface and atoms of type_b are forming the core of nanoparticle. The 'surface' of nanoparticle is defined as atoms with unfinished first coordination shell. Used algorithm without need for explicit knowledge of far coordination shells parameters, as it was required in CoreShellFCC(..) Parameters ---------- atoms: ase.Atoms ase Atoms object, containing atomic cluster. type_a: string Symbol of chemical element to be placed on the shell. type_b: string Symbol of chemical element to be placed in the core. ratio: float Guards the number of shell atoms, type_a:type_b = ratio:(1-ratio) R_min: float Typical bond length. Neighboring atoms within this value are counted as coordination numbers. Default is 3.0. CN_max: float Maximum possible coordination number (bulk coordination number). Default is 12. n_depth: int Number of layers of the shell formed by atoms ratio. Default value -1 is ignored and n_depth is calculated according ratio. If n_depth is set then value of ratio is ignored. Returns ------- Function returns ASE atoms object which contains bimetallic core-shell cluster Example -------- >>> atoms = FaceCenteredCubic('Ag', [(1, 0, 0), (1, 1, 0), (1, 1, 1)], [7,8,7], 4.09) >>> atoms = CoreShellCN(atoms, 'Pt', 'Ag', 0.5) >>> view(atoms) """ # 0 < ratio < 1 target_x = ratio if n_depth != -1: target_x = 1 n_atoms = len(atoms) n_a = (np.array(atoms.get_chemical_symbols()) == type_a).sum() #n_b = (atoms.get_chemical_symbols() == type_b).sum() #print n_a n_shell = 0 # depth of the shell while (n_a < n_atoms * target_x): n_shell += 1 print ("shell: ", n_shell) if (n_depth != -1)and(n_shell > n_depth): break neiblist = NeighborList( [ R_min ] * n_atoms, self_interaction=False, bothways=True ) neiblist.build( atoms ) for i in xrange( n_atoms ): indeces, offsets = neiblist.get_neighbors(i) if (atoms[i].symbol == type_b): CN_temp = 0 for ii in indeces: if atoms[ii].symbol == type_b: CN_temp += 1 #print "CN_temp: ", CN_temp if (CN_temp < CN_max): # coord shell is not full, swap type to type_a! atoms[i].tag = n_shell # not swap yet, but mark # swap atom types now. Stop if target ratio achieved for atom in atoms: if (atom.tag > 0)&(atom.symbol == type_b): if n_a < n_atoms * target_x: atom.symbol = type_a n_a += 1 #print "n_A: ", n_a # end while # check number of atoms checkn_a = 0 for element in atoms.get_chemical_symbols(): if element == type_a: checkn_a += 1 #print "Should be equal: ", n_a, checkn_a assert n_a == checkn_a return atoms
def test_nl(atoms, cutoff): cutoffs = [cutoff/2]*len(atoms) nl = NeighborList(cutoffs, skin=0, bothways=True, self_interaction=False) nl.update(atoms) for i in range(len(atoms)): (x, _) = nl.get_neighbors(i)
class EMT(Calculator): """Python implementation of the Effective Medium Potential. Supports the following standard EMT metals: Al, Cu, Ag, Au, Ni, Pd and Pt. In addition, the following elements are supported. They are NOT well described by EMT, and the parameters are not for any serious use: H, C, N, O The potential takes a single argument, ``asap_cutoff`` (default: False). If set to True, the cutoff mimics how Asap does it; most importantly the global cutoff is chosen from the largest atom present in the simulation, if False it is chosen from the largest atom in the parameter table. True gives the behaviour of the Asap code and older EMT implementations, although the results are not bitwise identical. """ implemented_properties = [ 'energy', 'forces', 'stress', 'magmom', 'magmoms' ] nolabel = True default_parameters = {'asap_cutoff': False} def __init__(self, **kwargs): Calculator.__init__(self, **kwargs) def initialize(self, atoms): self.par = {} self.rc = 0.0 self.numbers = atoms.get_atomic_numbers() if self.parameters.asap_cutoff: relevant_pars = {} for symb, p in parameters.items(): if atomic_numbers[symb] in self.numbers: relevant_pars[symb] = p else: relevant_pars = parameters maxseq = max(par[1] for par in relevant_pars.values()) * Bohr rc = self.rc = beta * maxseq * 0.5 * (sqrt(3) + sqrt(4)) rr = rc * 2 * sqrt(4) / (sqrt(3) + sqrt(4)) self.acut = np.log(9999.0) / (rr - rc) if self.parameters.asap_cutoff: self.rc_list = self.rc * 1.045 else: self.rc_list = self.rc + 0.5 for Z in self.numbers: if Z not in self.par: sym = chemical_symbols[Z] if sym not in parameters: raise NotImplementedError( 'No EMT-potential for {0}'.format(sym)) p = parameters[sym] s0 = p[1] * Bohr eta2 = p[3] / Bohr kappa = p[4] / Bohr x = eta2 * beta * s0 gamma1 = 0.0 gamma2 = 0.0 for i, n in enumerate([12, 6, 24]): r = s0 * beta * sqrt(i + 1) x = n / (12 * (1.0 + exp(self.acut * (r - rc)))) gamma1 += x * exp(-eta2 * (r - beta * s0)) gamma2 += x * exp(-kappa / beta * (r - beta * s0)) self.par[Z] = { 'E0': p[0], 's0': s0, 'V0': p[2], 'eta2': eta2, 'kappa': kappa, 'lambda': p[5] / Bohr, 'n0': p[6] / Bohr**3, 'rc': rc, 'gamma1': gamma1, 'gamma2': gamma2 } self.ksi = {} for s1, p1 in self.par.items(): self.ksi[s1] = {} for s2, p2 in self.par.items(): self.ksi[s1][s2] = p2['n0'] / p1['n0'] self.forces = np.empty((len(atoms), 3)) self.stress = np.empty((3, 3)) self.sigma1 = np.empty(len(atoms)) self.deds = np.empty(len(atoms)) self.nl = NeighborList([0.5 * self.rc_list] * len(atoms), self_interaction=False) def calculate(self, atoms=None, properties=['energy'], system_changes=all_changes): Calculator.calculate(self, atoms, properties, system_changes) if 'numbers' in system_changes: self.initialize(self.atoms) positions = self.atoms.positions numbers = self.atoms.numbers cell = self.atoms.cell self.nl.update(self.atoms) self.energy = 0.0 self.sigma1[:] = 0.0 self.forces[:] = 0.0 self.stress[:] = 0.0 natoms = len(self.atoms) for a1 in range(natoms): Z1 = numbers[a1] p1 = self.par[Z1] ksi = self.ksi[Z1] neighbors, offsets = self.nl.get_neighbors(a1) offsets = np.dot(offsets, cell) for a2, offset in zip(neighbors, offsets): d = positions[a2] + offset - positions[a1] r = sqrt(np.dot(d, d)) if r < self.rc_list: Z2 = numbers[a2] p2 = self.par[Z2] self.interact1(a1, a2, d, r, p1, p2, ksi[Z2]) for a in range(natoms): Z = numbers[a] p = self.par[Z] try: ds = -log(self.sigma1[a] / 12) / (beta * p['eta2']) except (OverflowError, ValueError): self.deds[a] = 0.0 self.energy -= p['E0'] continue x = p['lambda'] * ds y = exp(-x) z = 6 * p['V0'] * exp(-p['kappa'] * ds) self.deds[a] = ((x * y * p['E0'] * p['lambda'] + p['kappa'] * z) / (self.sigma1[a] * beta * p['eta2'])) self.energy += p['E0'] * ((1 + x) * y - 1) + z for a1 in range(natoms): Z1 = numbers[a1] p1 = self.par[Z1] ksi = self.ksi[Z1] neighbors, offsets = self.nl.get_neighbors(a1) offsets = np.dot(offsets, cell) for a2, offset in zip(neighbors, offsets): d = positions[a2] + offset - positions[a1] r = sqrt(np.dot(d, d)) if r < self.rc_list: Z2 = numbers[a2] p2 = self.par[Z2] self.interact2(a1, a2, d, r, p1, p2, ksi[Z2]) self.results['energy'] = self.energy self.results['free_energy'] = self.energy self.results['forces'] = self.forces if 'stress' in properties: if self.atoms.number_of_lattice_vectors == 3: self.stress += self.stress.T.copy() self.stress *= -0.5 / self.atoms.get_volume() self.results['stress'] = self.stress.flat[[0, 4, 8, 5, 2, 1]] else: raise PropertyNotImplementedError def interact1(self, a1, a2, d, r, p1, p2, ksi): x = exp(self.acut * (r - self.rc)) theta = 1.0 / (1.0 + x) y1 = (0.5 * p1['V0'] * exp(-p2['kappa'] * (r / beta - p2['s0'])) * ksi / p1['gamma2'] * theta) y2 = (0.5 * p2['V0'] * exp(-p1['kappa'] * (r / beta - p1['s0'])) / ksi / p2['gamma2'] * theta) self.energy -= y1 + y2 f = ((y1 * p2['kappa'] + y2 * p1['kappa']) / beta + (y1 + y2) * self.acut * theta * x) * d / r self.forces[a1] += f self.forces[a2] -= f self.stress -= np.outer(f, d) self.sigma1[a1] += (exp(-p2['eta2'] * (r - beta * p2['s0'])) * ksi * theta / p1['gamma1']) self.sigma1[a2] += (exp(-p1['eta2'] * (r - beta * p1['s0'])) / ksi * theta / p2['gamma1']) def interact2(self, a1, a2, d, r, p1, p2, ksi): x = exp(self.acut * (r - self.rc)) theta = 1.0 / (1.0 + x) y1 = (exp(-p2['eta2'] * (r - beta * p2['s0'])) * ksi / p1['gamma1'] * theta * self.deds[a1]) y2 = (exp(-p1['eta2'] * (r - beta * p1['s0'])) / ksi / p2['gamma1'] * theta * self.deds[a2]) f = ((y1 * p2['eta2'] + y2 * p1['eta2']) + (y1 + y2) * self.acut * theta * x) * d / r self.forces[a1] -= f self.forces[a2] += f self.stress += np.outer(f, d)
d = 0.0 for a in range(len(atoms)): i, offsets = nl.get_neighbors(a) for j in i: c[j] += 1 c[a] += len(i) d += (((R[i] + np.dot(offsets, cell) - R[a])**2).sum(1)**0.5).sum() return d, c for sorted in [False, True]: for p1 in range(2): for p2 in range(2): for p3 in range(2): # print(p1, p2, p3) atoms.set_pbc((p1, p2, p3)) nl = NeighborList(atoms.numbers * 0.2 + 0.5, skin=0.0, sorted=sorted) nl.update(atoms) d, c = count(nl, atoms) atoms2 = atoms.repeat((p1 + 1, p2 + 1, p3 + 1)) nl2 = NeighborList(atoms2.numbers * 0.2 + 0.5, skin=0.0, sorted=sorted) nl2.update(atoms2) d2, c2 = count(nl2, atoms2) c2.shape = (-1, 10) dd = d * (p1 + 1) * (p2 + 1) * (p3 + 1) - d2 # print(dd) # print(c2 - c) assert abs(dd) < 1e-10 assert not (c2 - c).any() h2 = Atoms('H2', positions=[(0, 0, 0), (0, 0, 1)])
def calculate( self, atoms=None, properties=None, system_changes=all_changes, ): if properties is None: properties = self.implemented_properties Calculator.calculate(self, atoms, properties, system_changes) natoms = len(self.atoms) sigma = self.parameters.sigma epsilon = self.parameters.epsilon rc = self.parameters.rc if self.nl is None or 'numbers' in system_changes: self.nl = NeighborList([rc / 2] * natoms, self_interaction=False) self.nl.update(self.atoms) positions = self.atoms.positions cell = self.atoms.cell # potential value at rc e0 = 4 * epsilon * ((sigma / rc)**12 - (sigma / rc)**6) atomic_energies = np.zeros(natoms) forces = np.zeros((natoms, 3)) stresses = np.zeros((natoms, 3, 3)) for ii in range(natoms): neighbors, offsets = self.nl.get_neighbors(ii) cells = np.dot(offsets, cell) # pointing *towards* neighbours distance_vectors = positions[neighbors] + cells - positions[ii] r2 = (distance_vectors**2).sum(1) c6 = (sigma**2 / r2)**3 c6[r2 > rc**2] = 0.0 c12 = c6**2 pairwise_energies = 4 * epsilon * (c12 - c6) - e0 * (c6 != 0.0) atomic_energies[ii] += 0.5 * pairwise_energies.sum( ) # atomic energies pairwise_forces = (-24 * epsilon * (2 * c12 - c6) / r2)[:, np.newaxis] * distance_vectors forces[ii] += pairwise_forces.sum(axis=0) stresses[ii] += 0.5 * np.dot( pairwise_forces.T, distance_vectors) # equivalent to outer product # add j < i contributions for jj, atom_j in enumerate(neighbors): atomic_energies[atom_j] += 0.5 * pairwise_energies[jj] forces[atom_j] += -pairwise_forces[jj] # f_ji = - f_ij stresses[atom_j] += 0.5 * np.outer(pairwise_forces[jj], distance_vectors[jj]) # no lattice, no stress if self.atoms.number_of_lattice_vectors == 3: stresses = full_3x3_to_voigt_6_stress(stresses) self.results['stress'] = (stresses.sum(axis=0) / self.atoms.get_volume()) self.results['stresses'] = stresses / self.atoms.get_volume() energy = atomic_energies.sum() self.results['energy'] = energy self.results['atomic_energies'] = atomic_energies self.results['free_energy'] = energy self.results['forces'] = forces
def calculate(self, crystal, ids=None): """Calculate and return the EAD. Parameters ---------- crystal: object ASE Structure object. ids: list A list of the centered atoms to be computed if None, all atoms will be considered Returns ------- d: dict The user-defined EAD that represent the crystal. d = {'x': [N, d], 'dxdr': [N, layer, d, 3], 'rdxdr': [N, layer, d, 3, 3], 'elements': list of elements} """ self.crystal = crystal self.total_atoms = len(crystal) # total atoms in the unit cell vol = crystal.get_volume() rc = [self.Rc / 2.] * self.total_atoms neighbors = NeighborList(rc, self_interaction=False, bothways=True, skin=0.) neighbors.update(crystal) unique_N = 0 for i in range(self.total_atoms): indices, offsets = neighbors.get_neighbors(i) ith = 0 if i not in indices: ith += 1 unique_N += len(np.unique(indices)) + ith # +1 is for i # Make numpy array here self.d = {'x': np.zeros([self.total_atoms, self.dsize]), 'elements': []} if self.derivative: self.d['dxdr'] = np.zeros([unique_N, self.dsize, 3]) self.d['seq'] = np.zeros([unique_N, 2], dtype=int) if self.stress: self.d['rdxdr'] = np.zeros([unique_N, self.dsize, 3, 3]) seq_count = 0 if ids is None: ids = range(len(crystal)) for i in ids: # range(self.total_atoms): element = crystal.get_chemical_symbols()[i] indices, offsets = neighbors.get_neighbors(i) Z = [] # atomic numbers of neighbors assert len(indices) > 0, \ f"There's no neighbor for this structure at Rc = {self.Rc} A." Ri = crystal.get_positions()[i] total_neighbors = len(indices) Rj = np.zeros([total_neighbors, 3]) IDs = np.zeros(total_neighbors, dtype=int) count = 0 for j, offset in zip(indices, offsets): Rj[count, :] = crystal.positions[j] + np.dot(offset, crystal.get_cell()) IDs[count] = j Z.append(crystal[j].number) count += 1 Z = np.array(Z) Rij = Rj - Ri Dij = np.sqrt(np.sum(Rij ** 2, axis=1)) d = calculate_eamd(i, self.total_atoms, Rij, Dij, Z, IDs, self.Rc, self.parameters, self.derivative, self.stress) self.d['x'][i] = d['x'] if self.derivative: n_seq = len(d['seq']) self.d['dxdr'][seq_count:seq_count + n_seq] = d['dxdr'] self.d['seq'][seq_count:seq_count + n_seq] = d['seq'] if self.stress: self.d['rdxdr'][seq_count:seq_count + n_seq] = -d['rdxdr'] / vol if self.derivative: seq_count += n_seq self.d['elements'].append(element) return self.d
def get_neighbours(atoms, r_cut, self_interaction=False): """Return a list of pairs of atoms within a given distance of each other. If matscipy can be imported, then this will directly call matscipy's neighbourlist function. Otherwise it will use ASE's NeighborList object. Args: atoms: ase.atoms object to calculate neighbours for r_cut: cutoff radius (float). Pairs of atoms are considered neighbours if they are within a distance r_cut of each other (note that this is double the parameter used in the ASE's neighborlist module) Returns: a tuple (i_list, j_list, d_list, fixed_atoms): i_list, j_list: i and j indices of each neighbour pair d_list: absolute distance between the corresponding pair fixed_atoms: indices of any fixed atoms """ if isinstance(atoms, Filter): atoms = atoms.atoms if have_matscipy: i_list, j_list, d_list = neighbour_list('ijd', atoms, r_cut) else: radii = [r_cut / 2 for i in range(len(atoms))] nl = NeighborList(radii, sorted=False, self_interaction=False, bothways=True) nl.update(atoms) i_list = [] j_list = [] d_list = [] for i, atom in enumerate(atoms): posn_i = atom.position indices, offsets = nl.get_neighbors(i) assert len(indices) == len(offsets) for j, offset in zip(indices, offsets): # Offsets represent how far away an atom is from its pair in terms # of the repeating cell - for instance, an atom i might be in cell # (0, 0, 0) while the neighbouring atom j is in cell (0, 1, 1). To # get the true position we have to correct for the offset: posn_j = atoms.positions[j] + np.dot(offset, atoms.get_cell()) distance = np.sqrt(((posn_j - posn_i)**2).sum()) i_list.append(i) j_list.append(j) d_list.append(distance) i_list = np.array(i_list) j_list = np.array(j_list) d_list = np.array(d_list) # filter out self-interactions (across PBC) if not self_interaction: mask = i_list != j_list i_list = i_list[mask] j_list = j_list[mask] d_list = d_list[mask] # filter out bonds where 1st atom (i) in pair is fixed fixed_atoms = [] for constraint in atoms.constraints: if isinstance(constraint, FixAtoms): fixed_atoms.extend(list(constraint.index)) return i_list, j_list, d_list, fixed_atoms
def biatomic(self, A, B, R1=3.0, calc_energy=False): r"""This routine analyzes atomic structure by the calculation of coordination numbers in cluster with atoms of two types (A and B). Parameters ---------- A: string atom type, like 'Ag', 'Pt', etc. B: string atom type, like 'Ag', 'Pt', etc. R1: float First coordination shell will icnlude all atoms with distance less then R1 [Angstrom]. Default value is 3. calc_energy: bool Flag used for calculation of potential energy with EMT calculator. The default value is False, so that energy is not calculated. Returns ------- N: int number of atoms in cluster nA: number of atoms of type A R: float radius of the cluster CN_AA: float average number of atoms A around atom A CN_AB: float average number of atoms A around atom B CN_BB: float average number of atoms B around atom B CN_BA: float average number of atoms B around atom A etha: float parameter of local ordering, -1 < etha < 1. Returns 999 if concentration of one of the component is too low. E: float potential energy NAcore: number of A atoms in core NBcore: number of B atoms in core CNshellAA: average CN of A-A for surface atoms only CNshellAB: average CN of A-B for surface atoms only CNshellBB: average CN of B-B for surface atoms only CNshellBA: average CN of B-A for surface atoms only Notes ----- The radius of the cluster is roughly determined as maximum the distance from the center to most distant atom in the cluster. Example -------- >>> atoms = FaceCenteredCubic('Ag', [(1, 0, 0), (1, 1, 0), (1, 1, 1)], [7,8,7], 4.09) >>> atoms = CoreShellFCC(atoms, 'Pt', 'Ag', 0.6, 4.09) >>> [N, nA, R, CN_AA, CN_AB, CN_BB, CN_BA, etha] = biatomic(atoms, 'Pt', 'Ag') >>> print "Short range order parameter: ", etha """ self.chems = [A, B] # for now used for report only N = len(self.atoms) nA = 0 nB = 0 for element in self.atoms.get_chemical_symbols(): if element == A: nA += 1 elif element == B: nB += 1 else: raise Exception('Extra element ' + element) if (nA + nB != N): raise Exception('Number of A (' + str(nA) + ') ' + 'and B (' + str(nB) + ') artoms mismatch!') nl = NeighborList([0.5 * R1] * N, self_interaction=False, bothways=True) nl.build(self.atoms) # initialize counters: CN_AA = 0 # averaged total coord. numbers CN_AB = 0 CN_BB = 0 CN_BA = 0 NAcore = 0 # number of atoms in core region NBcore = 0 CNshellAA = 0 # average coord. numbers for surface atoms CNshellAB = 0 CNshellBB = 0 CNshellBA = 0 for iatom in xrange(0, N): #print "central atom index:", iatom, " kind: ", self.atoms[iatom].symbol indeces, offsets = nl.get_neighbors(iatom) if self.atoms[iatom].symbol == B: CN_BB_temp = 0 CN_BA_temp = 0 for ii in indeces: #print "neighbor atom index:", ii, " kind: ", self.atoms[ii].symbol if self.atoms[ii].symbol == B: CN_BB_temp += 1 elif self.atoms[ii].symbol == A: CN_BA_temp += 1 else: print("Warning: unknown atom type %s. It will not be counted!"%self.atoms[ii].symbol) CN_BB += CN_BB_temp CN_BA += CN_BA_temp if len(indeces) < 12: # SHELL CNshellBB += CN_BB_temp CNshellBA += CN_BA_temp else: # CORE NBcore += 1 elif self.atoms[iatom].symbol == A: CN_AA_temp = 0 CN_AB_temp = 0 for i in indeces: #print "neighbor atom index:", i, " kind: ", self.atoms[i].symbol if self.atoms[i].symbol == A: CN_AA_temp += 1 elif self.atoms[i].symbol == B: CN_AB_temp += 1 else: print("Warning: unknown atom type %s. It will not be counted!"%self.atoms[i].symbol) CN_AA += CN_AA_temp CN_AB += CN_AB_temp if len(indeces) < 12: # SHELL CNshellAA += CN_AA_temp CNshellAB += CN_AB_temp else: # CORE NAcore += 1 else: #raise Exception("Un") print("Warning: unknown atom type %s. It will not be counted!"%self.atoms[iatom].symbol) # averaging: CN_AA = CN_AA * 1.0 / nA CN_AB = CN_AB * 1.0 / nA CN_BB = CN_BB * 1.0 / nB CN_BA = CN_BA * 1.0 / nB znam = (nA - NAcore) if znam > 0.0001: CNshellAA = CNshellAA * 1.0 / znam CNshellAB = CNshellAB * 1.0 / znam else: CNshellAA = 0 CNshellAB = 0 znam = (nB - NBcore) if znam > 0.0001: CNshellBB = CNshellBB * 1.0 / znam CNshellBA = CNshellBA * 1.0 / znam else: CNshellBB = 0 CNshellBA = 0 # calc concentrations: concB = nB * 1.0 / N znam = concB * (CN_AA + CN_AB) if znam < 0.0001: #print "WARNING! Too low B concentration: ",concB etha = 999 else: etha = 1 - CN_AB / znam R = self.atoms.positions.max() / 2.0 if calc_energy: #from asap3 import EMT from ase.calculators.emt import EMT self.atoms.set_calculator(EMT()) E = self.atoms.get_potential_energy() else: E = -1 #return N, nA, R, CN_AA, CN_AB, CN_BB, CN_BA, etha, E, NAcore, \ # NBcore, CNshellAA, CNshellAB, CNshellBB, CNshellBA self.N = N self.nA = nA self.R = R self.CN_AA = CN_AA #TODO: use only arrays of CNs self.CN_AB = CN_AB self.CN_BB = CN_BB self.CN_BA = CN_BA self.CNs = np.array([ [CN_AA, CN_AB], [CN_BA, CN_BB] ]) self.etha = etha self.E = E self.NAcore = NAcore self.NBcore = NBcore self.CNshellAA = CNshellAA self.CNshellAB = CNshellAB self.CNshellBB = CNshellBB self.CNshellBA = CNshellBA
def find_layers( # pylint: disable=too-many-locals,too-many-statements,too-many-branches asecell, factor=1.1): """ Obtains all subunits of a given structure by looking at the connectivity of the bonds. :param asecell: the bulk unit cell (in ase.Atoms format) :param factor: the skin factor :return: a tuple with a boolean indicating if the material is layered, a list of layers in the structure (ase format), a list of indices of the atoms in each layer, and a rotated bulk ASE cell (with stacking axis along z). MOREOVER, it 1) returns layers ordered by stacking index and 2) makes sure the layer is connected when removing the PBC along the third (stacking) axis. """ tol = 1.0e-6 nl = NeighborList( factor * get_covalent_radii_array(asecell), bothways=True, self_interaction=False, skin=0.0, ) nl.update(asecell) vector1, vector2, vector3 = asecell.cell is_layered = True layer_structures = [] layer_indices = [] visited = [] aselayer = None final_layered_structures = None # Loop over atoms (idx: atom index) for idx in range(len(asecell)): # pylint: disable=too-many-nested-blocks # Will contain the indices of the atoms in the "current" layer layer = [] # Check if I already visited this atom if idx not in visited: # Update 'layer' and 'visited' check_neighbors(idx, nl, asecell, visited, layer) aselayer = asecell.copy()[layer] layer_nl = NeighborList( factor * get_covalent_radii_array(aselayer), bothways=True, self_interaction=False, skin=0.0, ) layer_nl.update(aselayer) # We search for the periodic images of the first atom (idx=0) # that are connected to at least one atom of the connected layer neigh_vec = [] for idx2 in range(len(aselayer)): _, offsets = layer_nl.get_neighbors(idx2) for offset in offsets: if not all(offset == [0, 0, 0]): neigh_vec.append(offset) # We define the dimensionality as the rank dim = np.linalg.matrix_rank(neigh_vec) if dim == 2: cell = asecell.cell vectors = list(np.dot(neigh_vec, cell)) iv = shortest_vector_index(vectors) vector1 = vectors.pop(iv) iv = shortest_vector_index(vectors) vector2 = vectors.pop(iv) vector3 = np.cross(vector1, vector2) while np.linalg.norm(vector3) < tol: iv = shortest_vector_index(vectors) vector2 = vectors.pop(iv) vector3 = np.cross(vector1, vector2) vector1, vector2 = gauss_reduce(vector1, vector2) vector3 = np.cross(vector1, vector2) aselayer = _update_and_rotate_cell( aselayer, [vector1, vector2, vector3], [list(range(len(aselayer)))]) disconnected = [] for i in range(-3, 4): for j in range(-3, 4): for k in range(-3, 4): vector = i * cell[0] + j * cell[1] + k * cell[2] if np.dot(vector3, vector) > tol: disconnected.append(vector) iv = shortest_vector_index(disconnected) vector3 = disconnected[iv] layer_structures.append(aselayer) layer_indices.append(layer) else: is_layered = False if is_layered: newcell = [vector1, vector2, vector3] if abs(np.linalg.det(newcell) / np.linalg.det(cell) - 1.0) > 1e-3: raise ValueError( "An error occurred. The new cell after rotation has a different volume than the original cell" ) rotated_asecell = _update_and_rotate_cell(asecell, newcell, layer_indices) # Re-order layers according to their projection # on the stacking direction vert_direction = np.cross(rotated_asecell.cell[0], rotated_asecell.cell[1]) vert_direction /= np.linalg.norm(vert_direction) stack_proj = [ np.dot(layer.positions, vert_direction).mean() for layer in [rotated_asecell[il] for il in layer_indices] ] stack_order = np.argsort(stack_proj) # order layers with increasing coordinate along the stacking direction layer_indices = [layer_indices[il] for il in stack_order] # Move the atoms along the third lattice vector so that # the first layer has zero projection along the vertical direction trans_vector = -(stack_proj[stack_order[0]] / np.dot( vert_direction, rotated_asecell.cell[2]) * rotated_asecell.cell[2]) rotated_asecell.translate(trans_vector) # I don't return the 'layer_structures' because there the atoms are moved # from their positions and the z axis lenght might not be appropriate final_layered_structures = [ rotated_asecell[this_layer_indices] for this_layer_indices in layer_indices ] else: rotated_asecell = None if not is_layered: aselayer = None return is_layered, final_layered_structures, layer_indices, rotated_asecell
def monoatomic(self, R1=3, calc_energy=False): r"""This routine analyzes atomic structure by the calculation of coordination numbers in cluster with only one type of atom. Parameters ---------- R1: float First coordination shell will icnlude all atoms with distance less then R1 [Angstrom]. Default value is 3. calc_energy: bool Flag used for calculation of potential energy with EMT calculator. The default value is False, so that energy is not calculated. Returns ------- N: int number of atoms in cluster R: float radius of the cluster CN: float average coord number E: float potential energy, -1 if calc_energy is False Ncore: number of atoms in core region (number of atoms with all 12 neighbors) CNshell: average coordination number for surface atoms only Notes ----- The radius of the cluster is roughly determined as maximum the distance from the center to most distant atom in the cluster. Example -------- >>> atoms = FaceCenteredCubic('Ag', [(1, 0, 0), (1, 1, 0), (1, 1, 1)], [7,8,7], 4.09) >>> qsar = QSAR(atoms) >>> qsar.monoatomic(R1=3.0) >>> print "average CN is ", qsar.CN """ self.chems = ['*'] # any element. For now used for report only N = len(self.atoms) nl = NeighborList( [0.5 * R1] * N, self_interaction=False, bothways=True ) nl.build(self.atoms) CN = 0 Ncore = 0 Nshell = 0 CNshell = 0 # average CN of surface atoms for i in xrange(0, N): indeces, offsets = nl.get_neighbors(i) CN += len(indeces) if len(indeces) < 12: Nshell += 1 CNshell += len(indeces) else: Ncore += 1 CN = CN * 1.0 / N CNshell = CNshell * 1.0 / Nshell #atoms.center() R = self.atoms.positions.max() / 2.0 if calc_energy: #from asap3 import EMT from ase.calculators.emt import EMT atoms.set_calculator(EMT()) E = atoms.get_potential_energy() else: E = -1 #return N, R, CN, E, Ncore, CNshell self.N = N #TODO: use array property CNs self.R = R self.CN = CN self.CNs = np.array([[CN]]) self.E = E self.Ncore = Ncore self.CNshell = CNshell
def __init__(self, bopatoms: BOPAtoms, cutoffs: list, **kwargs): self.bopatoms = bopatoms self.nl = NeighborList(cutoffs=cutoffs, bothways=True, self_interaction=False, **kwargs) self.nl.update(bopatoms)
class EAM(Calculator): r""" EAM Interface Documentation Introduction ============ The Embedded Atom Method (EAM) [1]_ is a classical potential which is good for modelling metals, particularly fcc materials. Because it is an equiaxial potential the EAM does not model directional bonds well. However, the Angular Dependent Potential (ADP) [2]_ which is an extended version of EAM is able to model directional bonds and is also included in the EAM calculator. Generally all that is required to use this calculator is to supply a potential file or as a set of functions that describe the potential. The files containing the potentials for this calculator are not included but many suitable potentials can be downloaded from The Interatomic Potentials Repository Project at http://www.ctcms.nist.gov/potentials/ Theory ====== A single element EAM potential is defined by three functions: the embedded energy, electron density and the pair potential. A two element alloy contains the individual three functions for each element plus cross pair interactions. The ADP potential has two additional sets of data to define the dipole and quadrupole directional terms for each alloy and their cross interactions. The total energy `E_{\rm tot}` of an arbitrary arrangement of atoms is given by the EAM potential as .. math:: E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij}) and .. math:: \bar\rho_i = \sum_j \rho(r_{ij}) where `F` is an embedding function, namely the energy to embed an atom `i` in the combined electron density `\bar\rho_i` which is contributed from each of its neighbouring atoms `j` by an amount `\rho(r_{ij})`, `\phi(r_{ij})` is the pair potential function representing the energy in bond `ij` which is due to the short-range electro-static interaction between atoms, and `r_{ij}` is the distance between an atom and its neighbour for that bond. The ADP potential is defined as .. math:: E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij}) + \frac{1}{2} \sum_{i,\alpha} (\mu_i^\alpha)^2 + \frac{1}{2} \sum_{i,\alpha,\beta} (\lambda_i^{\alpha\beta})^2 - \frac{1}{6} \sum_i \nu_i^2 where `\mu_i^\alpha` is the dipole vector, `\lambda_i^{\alpha\beta}` is the quadrupole tensor and `\nu_i` is the trace of `\lambda_i^{\alpha\beta}`. Running the Calculator ====================== EAM calculates the cohesive atom energy and forces. Internally the potential functions are defined by splines which may be directly supplied or created by reading the spline points from a data file from which a spline function is created. The LAMMPS compatible ``.alloy`` and ``.adp`` formats are supported. The LAMMPS ``.eam`` format is slightly different from the ``.alloy`` format and is currently not supported. For example:: from ase.calculators.eam import EAM mishin = EAM(potential='Al99.eam.alloy') mishin.write_potential('new.eam.alloy') mishin.plot() slab.set_calculator(mishin) slab.get_potential_energy() slab.get_forces() The breakdown of energy contribution from the indvidual components are stored in the calculator instance ``.results['energy_components']`` Arguments ========= ========================= ==================================================== Keyword Description ========================= ==================================================== ``potential`` file of potential in ``.alloy`` or ``.adp`` format (This is generally all you need to supply) ``elements[N]`` array of N element abbreviations ``embedded_energy[N]`` arrays of embedded energy functions ``electron_density[N]`` arrays of electron density functions ``phi[N,N]`` arrays of pair potential functions ``d_embedded_energy[N]`` arrays of derivative embedded energy functions ``d_electron_density[N]`` arrays of derivative electron density functions ``d_phi[N,N]`` arrays of derivative pair potentials functions ``d[N,N], q[N,N]`` ADP dipole and quadrupole function ``d_d[N,N], d_q[N,N]`` ADP dipole and quadrupole derivative functions ``skin`` skin distance passed to NeighborList(). If no atom has moved more than the skin-distance since the last call to the ``update()`` method then the neighbor list can be reused. Defaults to 1.0. ``form`` the form of the potential ``alloy`` or ``adp``. This will be determined from the file suffix or must be set if using equations ========================= ==================================================== Additional parameters for writing potential files ================================================= The following parameters are only required for writing a potential in ``.alloy`` or ``.adp`` format file. ========================= ==================================================== Keyword Description ========================= ==================================================== ``header`` Three line text header. Default is standard message. ``Z[N]`` Array of atomic number of each element ``mass[N]`` Atomic mass of each element ``a[N]`` Array of lattice parameters for each element ``lattice[N]`` Lattice type ``nrho`` No. of rho samples along embedded energy curve ``drho`` Increment for sampling density ``nr`` No. of radial points along density and pair potential curves ``dr`` Increment for sampling radius ========================= ==================================================== Special features ================ ``.plot()`` Plots the individual functions. This may be called from multiple EAM potentials to compare the shape of the individual curves. This function requires the installation of the Matplotlib libraries. Notes/Issues ============= * Although currently not fast, this calculator can be good for trying small calculations or for creating new potentials by matching baseline data such as from DFT results. The format for these potentials is compatible with LAMMPS_ and so can be used either directly by LAMMPS or with the ASE LAMMPS calculator interface. * Supported formats are the LAMMPS_ ``.alloy`` and ``.adp``. The ``.eam`` format is currently not supported. The form of the potential will be determined from the file suffix. * Any supplied values will override values read from the file. * The derivative functions, if supplied, are only used to calculate forces. * There is a bug in early versions of scipy that will cause eam.py to crash when trying to evaluate splines of a potential with one neighbor such as caused by evaluating a dimer. .. _LAMMPS: http://lammps.sandia.gov/ .. [1] M.S. Daw and M.I. Baskes, Phys. Rev. Letters 50 (1983) 1285. .. [2] Y. Mishin, M.J. Mehl, and D.A. Papaconstantopoulos, Acta Materialia 53 2005 4029--4041. End EAM Interface Documentation """ implemented_properties = ['energy', 'forces'] default_parameters = dict( skin=1.0, potential=None, header=[b'EAM/ADP potential file\n', b'Generated from eam.py\n', b'blank\n']) def __init__(self, restart=None, ignore_bad_restart_file=False, label=os.curdir, atoms=None, **kwargs): if 'potential' in kwargs: self.read_potential(kwargs['potential']) Calculator.__init__(self, restart, ignore_bad_restart_file, label, atoms, **kwargs) valid_args = ('potential', 'elements', 'header', 'drho', 'dr', 'cutoff', 'atomic_number', 'mass', 'a', 'lattice', 'embedded_energy', 'electron_density', 'phi', # derivatives 'd_embedded_energy', 'd_electron_density', 'd_phi', 'd', 'q', 'd_d', 'd_q', # adp terms 'skin', 'form', 'Z', 'nr', 'nrho', 'mass') # set any additional keyword arguments for arg, val in self.parameters.items(): if arg in valid_args: setattr(self, arg, val) else: raise RuntimeError('unknown keyword arg "%s" : not in %s' % (arg, valid_args)) def set_form(self, fileobj): """set the form variable based on the file name suffix""" extension = os.path.splitext(fileobj)[1] if extension == '.eam': self.form = 'eam' elif extension == '.alloy': self.form = 'alloy' elif extension == '.adp': self.form = 'adp' else: raise RuntimeError('unknown file extension type: %s' % extension) def read_potential(self, fileobj): """Reads a LAMMPS EAM file in alloy or adp format and creates the interpolation functions from the data """ if isinstance(fileobj, basestring): f = open(fileobj) self.set_form(fileobj) else: f = fileobj def lines_to_list(lines): """Make the data one long line so as not to care how its formatted """ data = [] for line in lines: data.extend(line.split()) return data lines = f.readlines() if self.form == 'eam': # single element eam file (aka funcfl) self.header = lines[:1] data = lines_to_list(lines[1:]) # eam form is just like an alloy form for one element self.Nelements = 1 self.Z = np.array([data[0]], dtype=int) self.mass = np.array([data[1]]) self.a = np.array([data[2]]) self.lattice = [data[3]] self.nrho = int(data[4]) self.drho = float(data[5]) self.nr = int(data[6]) self.dr = float(data[7]) self.cutoff = float(data[8]) n = 9 + self.nrho self.embedded_data = np.array([np.float_(data[9:n])]) self.rphi_data = np.zeros([self.Nelements, self.Nelements, self.nr]) effective_charge = np.float_(data[n:n + self.nr]) # convert effective charges to rphi according to # http://lammps.sandia.gov/doc/pair_eam.html self.rphi_data[0, 0] = Bohr * Hartree * (effective_charge**2) self.density_data = np.array( [np.float_(data[n + self.nr:n + 2 * self.nr])]) else: self.header = lines[:3] i = 3 data = lines_to_list(lines[i:]) self.Nelements = int(data[0]) d = 1 self.elements = data[d:d + self.Nelements] d += self.Nelements self.nrho = int(data[d]) self.drho = float(data[d + 1]) self.nr = int(data[d + 2]) self.dr = float(data[d + 3]) self.cutoff = float(data[d + 4]) self.embedded_data = np.zeros([self.Nelements, self.nrho]) self.density_data = np.zeros([self.Nelements, self.nr]) self.Z = np.zeros([self.Nelements], dtype=int) self.mass = np.zeros([self.Nelements]) self.a = np.zeros([self.Nelements]) self.lattice = [] d += 5 # reads in the part of the eam file for each element for elem in range(self.Nelements): self.Z[elem] = int(data[d]) self.mass[elem] = float(data[d + 1]) self.a[elem] = float(data[d + 2]) self.lattice.append(data[d + 3]) d += 4 self.embedded_data[elem] = np.float_( data[d:(d + self.nrho)]) d += self.nrho self.density_data[elem] = np.float_(data[d:(d + self.nr)]) d += self.nr # reads in the r*phi data for each interaction between elements self.rphi_data = np.zeros([self.Nelements, self.Nelements, self.nr]) for i in range(self.Nelements): for j in range(i + 1): self.rphi_data[j, i] = np.float_(data[d:(d + self.nr)]) d += self.nr self.r = np.arange(0, self.nr) * self.dr self.rho = np.arange(0, self.nrho) * self.drho self.set_splines() if (self.form == 'adp'): self.read_adp_data(data, d) self.set_adp_splines() def set_splines(self): # this section turns the file data into three functions (and # derivative functions) that define the potential self.embedded_energy = np.empty(self.Nelements, object) self.electron_density = np.empty(self.Nelements, object) self.d_embedded_energy = np.empty(self.Nelements, object) self.d_electron_density = np.empty(self.Nelements, object) for i in range(self.Nelements): self.embedded_energy[i] = spline(self.rho, self.embedded_data[i], k=3) self.electron_density[i] = spline(self.r, self.density_data[i], k=3) self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i]) self.d_electron_density[i] = self.deriv(self.electron_density[i]) self.phi = np.empty([self.Nelements, self.Nelements], object) self.d_phi = np.empty([self.Nelements, self.Nelements], object) # ignore the first point of the phi data because it is forced # to go through zero due to the r*phi format in alloy and adp for i in range(self.Nelements): for j in range(i, self.Nelements): self.phi[i, j] = spline( self.r[1:], self.rphi_data[i, j][1:] / self.r[1:], k=3) self.d_phi[i, j] = self.deriv(self.phi[i, j]) if j != i: self.phi[j, i] = self.phi[i, j] self.d_phi[j, i] = self.d_phi[i, j] def set_adp_splines(self): self.d = np.empty([self.Nelements, self.Nelements], object) self.d_d = np.empty([self.Nelements, self.Nelements], object) self.q = np.empty([self.Nelements, self.Nelements], object) self.d_q = np.empty([self.Nelements, self.Nelements], object) for i in range(self.Nelements): for j in range(i, self.Nelements): self.d[i, j] = spline(self.r[1:], self.d_data[i, j][1:], k=3) self.d_d[i, j] = self.deriv(self.d[i, j]) self.q[i, j] = spline(self.r[1:], self.q_data[i, j][1:], k=3) self.d_q[i, j] = self.deriv(self.q[i, j]) # make symmetrical if j != i: self.d[j, i] = self.d[i, j] self.d_d[j, i] = self.d_d[i, j] self.q[j, i] = self.q[i, j] self.d_q[j, i] = self.d_q[i, j] def read_adp_data(self, data, d): """read in the extra adp data from the potential file""" self.d_data = np.zeros([self.Nelements, self.Nelements, self.nr]) # should be non symmetrical combinations of 2 for i in range(self.Nelements): for j in range(i + 1): self.d_data[j, i] = data[d:d + self.nr] d += self.nr self.q_data = np.zeros([self.Nelements, self.Nelements, self.nr]) # should be non symmetrical combinations of 2 for i in range(self.Nelements): for j in range(i + 1): self.q_data[j, i] = data[d:d + self.nr] d += self.nr def write_potential(self, filename, nc=1, numformat='%.8e'): """Writes out the potential in the format given by the form variable to 'filename' with a data format that is nc columns wide. Note: array lengths need to be an exact multiple of nc """ f = open(filename, 'wb') assert self.nr % nc == 0 assert self.nrho % nc == 0 for line in self.header: f.write(line) f.write('{0} '.format(self.Nelements).encode()) f.write(' '.join(self.elements).encode() + b'\n') f.write(('%d %f %d %f %f \n' % (self.nrho, self.drho, self.nr, self.dr, self.cutoff)).encode()) # start of each section for each element # rs = np.linspace(0, self.nr * self.dr, self.nr) # rhos = np.linspace(0, self.nrho * self.drho, self.nrho) rs = np.arange(0, self.nr) * self.dr rhos = np.arange(0, self.nrho) * self.drho for i in range(self.Nelements): f.write(('%d %f %f %s\n' % (self.Z[i], self.mass[i], self.a[i], str(self.lattice[i]))).encode()) np.savetxt(f, self.embedded_energy[i](rhos).reshape(self.nrho // nc, nc), fmt=nc * [numformat]) np.savetxt(f, self.electron_density[i](rs).reshape(self.nr // nc, nc), fmt=nc * [numformat]) # write out the pair potentials in Lammps DYNAMO setfl format # as r*phi for alloy format for i in range(self.Nelements): for j in range(i, self.Nelements): np.savetxt(f, (rs * self.phi[i, j](rs)).reshape(self.nr // nc, nc), fmt=nc * [numformat]) if self.form == 'adp': # these are the u(r) or dipole values for i in range(self.Nelements): for j in range(i + 1): np.savetxt(f, self.d_data[i, j]) # these are the w(r) or quadrupole values for i in range(self.Nelements): for j in range(i + 1): np.savetxt(f, self.q_data[i, j]) f.close() def update(self, atoms): # check all the elements are available in the potential self.Nelements = len(self.elements) elements = np.unique(atoms.get_chemical_symbols()) unavailable = np.logical_not( np.array([item in self.elements for item in elements])) if np.any(unavailable): raise RuntimeError('These elements are not in the potential: %s' % elements[unavailable]) # cutoffs need to be a vector for NeighborList cutoffs = self.cutoff * np.ones(len(atoms)) # convert the elements to an index of the position # in the eam format self.index = np.array([self.elements.index(el) for el in atoms.get_chemical_symbols()]) self.pbc = atoms.get_pbc() # since we need the contribution of all neighbors to the # local electron density we cannot just calculate and use # one way neighbors self.neighbors = NeighborList(cutoffs, skin=self.parameters.skin, self_interaction=False, bothways=True) self.neighbors.update(atoms) def calculate(self, atoms=None, properties=['energy'], system_changes=all_changes): """EAM Calculator atoms: Atoms object Contains positions, unit-cell, ... properties: list of str List of what needs to be calculated. Can be any combination of 'energy', 'forces' system_changes: list of str List of what has changed since last calculation. Can be any combination of these five: 'positions', 'numbers', 'cell', 'pbc', 'initial_charges' and 'initial_magmoms'. """ Calculator.calculate(self, atoms, properties, system_changes) # we shouldn't really recalc if charges or magmos change if len(system_changes) > 0: # something wrong with this way self.update(self.atoms) self.calculate_energy(self.atoms) if 'forces' in properties: self.calculate_forces(self.atoms) # check we have all the properties requested for property in properties: if property not in self.results: if property is 'energy': self.calculate_energy(self.atoms) if property is 'forces': self.calculate_forces(self.atoms) # we need to remember the previous state of parameters # if 'potential' in parameter_changes and potential != None: # self.read_potential(potential) def calculate_energy(self, atoms): """Calculate the energy the energy is made up of the ionic or pair interaction and the embedding energy of each atom into the electron cloud generated by its neighbors """ pair_energy = 0.0 embedding_energy = 0.0 mu_energy = 0.0 lam_energy = 0.0 trace_energy = 0.0 self.total_density = np.zeros(len(atoms)) if (self.form == 'adp'): self.mu = np.zeros([len(atoms), 3]) self.lam = np.zeros([len(atoms), 3, 3]) for i in range(len(atoms)): # this is the atom to be embedded neighbors, offsets = self.neighbors.get_neighbors(i) offset = np.dot(offsets, atoms.get_cell()) rvec = (atoms.positions[neighbors] + offset - atoms.positions[i]) # calculate the distance to the nearest neighbors r = np.sqrt(np.sum(np.square(rvec), axis=1)) # fast # r = np.apply_along_axis(np.linalg.norm, 1, rvec) # sloow nearest = np.arange(len(r))[r <= self.cutoff] for j_index in range(self.Nelements): use = self.index[neighbors[nearest]] == j_index if not use.any(): continue pair_energy += np.sum(self.phi[self.index[i], j_index]( r[nearest][use])) / 2. density = np.sum( self.electron_density[j_index](r[nearest][use])) self.total_density[i] += density if self.form == 'adp': self.mu[i] += self.adp_dipole( r[nearest][use], rvec[nearest][use], self.d[self.index[i], j_index]) self.lam[i] += self.adp_quadrupole( r[nearest][use], rvec[nearest][use], self.q[self.index[i], j_index]) # add in the electron embedding energy embedding_energy += self.embedded_energy[self.index[i]]( self.total_density[i]) components = dict(pair=pair_energy, embedding=embedding_energy) if self.form == 'adp': mu_energy += np.sum(self.mu ** 2) / 2. lam_energy += np.sum(self.lam ** 2) / 2. for i in range(len(atoms)): # this is the atom to be embedded trace_energy -= np.sum(self.lam[i].trace() ** 2) / 6. adp_result = dict(adp_mu=mu_energy, adp_lam=lam_energy, adp_trace=trace_energy) components.update(adp_result) self.positions = atoms.positions.copy() self.cell = atoms.get_cell().copy() energy = 0.0 for i in components.keys(): energy += components[i] self.energy_free = energy self.energy_zero = energy self.results['energy_components'] = components self.results['energy'] = energy def calculate_forces(self, atoms): # calculate the forces based on derivatives of the three EAM functions self.update(atoms) self.results['forces'] = np.zeros((len(atoms), 3)) for i in range(len(atoms)): # this is the atom to be embedded neighbors, offsets = self.neighbors.get_neighbors(i) offset = np.dot(offsets, atoms.get_cell()) # create a vector of relative positions of neighbors rvec = atoms.positions[neighbors] + offset - atoms.positions[i] r = np.sqrt(np.sum(np.square(rvec), axis=1)) nearest = np.arange(len(r))[r < self.cutoff] d_embedded_energy_i = self.d_embedded_energy[ self.index[i]](self.total_density[i]) urvec = rvec.copy() # unit directional vector for j in np.arange(len(neighbors)): urvec[j] = urvec[j] / r[j] for j_index in range(self.Nelements): use = self.index[neighbors[nearest]] == j_index if not use.any(): continue rnuse = r[nearest][use] density_j = self.total_density[neighbors[nearest][use]] scale = (self.d_phi[self.index[i], j_index](rnuse) + (d_embedded_energy_i * self.d_electron_density[j_index](rnuse)) + (self.d_embedded_energy[j_index](density_j) * self.d_electron_density[self.index[i]](rnuse))) self.results['forces'][i] += np.dot(scale, urvec[nearest][use]) if (self.form == 'adp'): adp_forces = self.angular_forces( self.mu[i], self.mu[neighbors[nearest][use]], self.lam[i], self.lam[neighbors[nearest][use]], rnuse, rvec[nearest][use], self.index[i], j_index) self.results['forces'][i] += adp_forces def angular_forces(self, mu_i, mu, lam_i, lam, r, rvec, form1, form2): # calculate the extra components for the adp forces # rvec are the relative positions to atom i psi = np.zeros(mu.shape) for gamma in range(3): term1 = (mu_i[gamma] - mu[:, gamma]) * self.d[form1][form2](r) term2 = np.sum((mu_i - mu) * self.d_d[form1][form2](r)[:, np.newaxis] * (rvec * rvec[:, gamma][:, np.newaxis] / r[:, np.newaxis]), axis=1) term3 = 2 * np.sum((lam_i[:, gamma] + lam[:, :, gamma]) * rvec * self.q[form1][form2](r)[:, np.newaxis], axis=1) term4 = 0.0 for alpha in range(3): for beta in range(3): rs = rvec[:, alpha] * rvec[:, beta] * rvec[:, gamma] term4 += ((lam_i[alpha, beta] + lam[:, alpha, beta]) * self.d_q[form1][form2](r) * rs) / r term5 = ((lam_i.trace() + lam.trace(axis1=1, axis2=2)) * (self.d_q[form1][form2](r) * r + 2 * self.q[form1][form2](r)) * rvec[:, gamma]) / 3. # the minus for term5 is a correction on the adp # formulation given in the 2005 Mishin Paper and is posted # on the NIST website with the AlH potential psi[:, gamma] = term1 + term2 + term3 + term4 - term5 return np.sum(psi, axis=0) def adp_dipole(self, r, rvec, d): # calculate the dipole contribution mu = np.sum((rvec * d(r)[:, np.newaxis]), axis=0) return mu # sign to agree with lammps def adp_quadrupole(self, r, rvec, q): # slow way of calculating the quadrupole contribution r = np.sqrt(np.sum(rvec ** 2, axis=1)) lam = np.zeros([rvec.shape[0], 3, 3]) qr = q(r) for alpha in range(3): for beta in range(3): lam[:, alpha, beta] += qr * rvec[:, alpha] * rvec[:, beta] return np.sum(lam, axis=0) def deriv(self, spline): """Wrapper for extracting the derivative from a spline""" def d_spline(aspline): return spline(aspline, 1) return d_spline def plot(self, name=''): """Plot the individual curves""" try: import matplotlib.pyplot as plt except ImportError: raise NotAvailable('This needs matplotlib module.') if self.form == 'eam' or self.form == 'alloy': nrow = 2 elif self.form == 'adp': nrow = 3 else: raise RuntimeError('Unknown form of potential: %s' % self.form) if hasattr(self, 'r'): r = self.r else: r = np.linspace(0, self.cutoff, 50) if hasattr(self, 'rho'): rho = self.rho else: rho = np.linspace(0, 10.0, 50) plt.subplot(nrow, 2, 1) self.elem_subplot(rho, self.embedded_energy, r'$\rho$', r'Embedding Energy $F(\bar\rho)$', name, plt) plt.subplot(nrow, 2, 2) self.elem_subplot(r, self.electron_density, r'$r$', r'Electron Density $\rho(r)$', name, plt) plt.subplot(nrow, 2, 3) self.multielem_subplot(r, self.phi, r'$r$', r'Pair Potential $\phi(r)$', name, plt) plt.ylim(-1.0, 1.0) # need reasonable values if self.form == 'adp': plt.subplot(nrow, 2, 5) self.multielem_subplot(r, self.d, r'$r$', r'Dipole Energy', name, plt) plt.subplot(nrow, 2, 6) self.multielem_subplot(r, self.q, r'$r$', r'Quadrupole Energy', name, plt) plt.plot() def elem_subplot(self, curvex, curvey, xlabel, ylabel, name, plt): plt.xlabel(xlabel) plt.ylabel(ylabel) for i in np.arange(self.Nelements): label = name + ' ' + self.elements[i] plt.plot(curvex, curvey[i](curvex), label=label) plt.legend() def multielem_subplot(self, curvex, curvey, xlabel, ylabel, name, plt): plt.xlabel(xlabel) plt.ylabel(ylabel) for i in np.arange(self.Nelements): for j in np.arange(i + 1): label = name + ' ' + self.elements[i] + '-' + self.elements[j] plt.plot(curvex, curvey[i, j](curvex), label=label) plt.legend()
def calculate(self, crystal, system=None): """The symmetry functions are obtained through this `calculate` method. Parameters ---------- crystal: object ASE Structure object. system: list A list of the crystal structures system. All elements in the list have to be integer. For example, the system of crystal structure is NaCl system. Then, system should be pass as [11, 17] Returns ------- all_G: dict The user-defined symmetry functions that represent the crystal. Currently, there are 3 types of symmetry functions implemented. Here are the order of the descriptors are printed out based on their symmetry parameters: - G2: ["element", "Rs", "eta"] - G4: ["pair_elements", "eta", "lambda", "zeta"] - G5: ["pair_elements", "eta", "lambda", "zeta"] """ self.crystal = crystal atomic_numbers = np.array(crystal.get_atomic_numbers()) vol = crystal.get_volume() if system is None: type_set1 = create_type_set(atomic_numbers, 1) # l type_set2 = create_type_set(atomic_numbers, 2) # l else: system = np.array(system, dtype=int) type_set1 = create_type_set(system, 1) # l type_set2 = create_type_set(system, 2) # l # Initialize dict for the symmetry functions self.all_G = {'x': [], 'elements': []} if self.derivative: self.all_G['dxdr'] = None self.all_G['seq'] = None if self.stress: self.all_G['rdxdr'] = None # Obtain neighbors info. rc = [self.Rc / 2] * len(self.crystal) neighbors = NeighborList(rc, self_interaction=False, bothways=True, skin=0.0) neighbors.update(crystal) for i in range(len(crystal)): element = crystal.get_chemical_symbols()[i] indices, offsets = neighbors.get_neighbors(i) assert len(indices)>0, \ f"There's no neighbor for this structure at Rc = {self.Rc} A." Ri = crystal.get_positions()[i] total_neighbors = len( indices) # total number of neighbors of atom i Rj = np.zeros([total_neighbors, 3]) Dij = np.zeros(total_neighbors) IDs = np.zeros(total_neighbors, dtype=int) jks = np.array(list(combinations(range(total_neighbors), 2))) count = 0 for j, offset in zip(indices, offsets): Rj[count, :] = crystal.positions[j] + np.dot( offset, crystal.get_cell()) Dij[count] = np.sqrt(sum((Rj[count, :] - Ri)**2)) IDs[count] = j count += 1 Rij = Rj - Ri Gi = [] GiPrime = None GiPrime_seq = None if self.G2_parameters is not None: G2i = calculate_G2(Dij, IDs, atomic_numbers, type_set1, self.Rc, self.G2_parameters, self._type) Gi = np.append(Gi, G2i) if self.derivative: seq, G2iP, rG2iP = calculate_G2Prime( Rij, Ri, i, IDs, atomic_numbers, type_set1, self.Rc, self.G2_parameters, self._type) if GiPrime is None: GiPrime = G2iP #[N1, l, 3] GiPrime_seq = seq if self.stress: rGiPrime = rG2iP else: GiPrime_seq = np.append(GiPrime_seq, seq, axis=0) GiPrime = np.append(GiPrime, G2iP, axis=1) if self.stress: rGiPrime = np.append(rGiPrime, rG2iP, axis=1) if self.G4_parameters is not None: G4i = calculate_G4(Rij, IDs, jks, atomic_numbers, type_set2, self.Rc, self.G4_parameters, self._type) Gi = np.append(Gi, G4i) if self.derivative: seq, G4iP, rG4iP = calculate_G4Prime( Rij, Ri, i, IDs, jks, atomic_numbers, type_set2, self.Rc, self.G4_parameters, self._type) if GiPrime is None: GiPrime = G4iP GiPrime_seq = seq if self.stress: rGiPrime = rG4iP else: #GiPrime_seq = np.append(GiPrime_seq, seq, axis=0) GiPrime = np.append(GiPrime, G4iP, axis=1) if self.stress: rGiPrime = np.append(rGiPrime, rG4iP, axis=1) #print(seq.shape, G4iP.shape, rG4iP.shape) #print(GiPrime_seq.shape, GiPrime.shape, rGiPrime.shape) if self.G5_parameters is not None: G5i = calculate_G5(Rij, IDs, jks, atomic_numbers, type_set2, self.Rc, self.G5_parameters, self._type) Gi = np.append(Gi, G5i) if self.derivative: seq, G5iP, rG5iP = calculate_G5Prime( Rij, Ri, i, IDs, jks, atomic_numbers, type_set2, self.Rc, self.G5_parameters, self._type) if GiPrime is None: GiPrime = G5iP GiPrime_seq = seq if self.stress: rGiPrime = rG5iP else: GiPrime = np.append(GiPrime, G5iP, axis=1) if self.stress: rGiPrime = np.append(rGiPrime, rG5iP, axis=1) self.all_G['x'].append(Gi) self.all_G['elements'].append(element) if self.derivative: if self.all_G['seq'] is None: self.all_G['seq'] = GiPrime_seq self.all_G['dxdr'] = GiPrime if self.stress: self.all_G['rdxdr'] = rGiPrime else: self.all_G['seq'] = np.append(self.all_G['seq'], GiPrime_seq, axis=0) self.all_G['dxdr'] = np.append(self.all_G['dxdr'], GiPrime, axis=0) if self.stress: self.all_G['rdxdr'] = np.append(self.all_G['rdxdr'], rGiPrime, axis=0) self.all_G['x'] = np.asarray(self.all_G['x']) if self.derivative: self.all_G['dxdr'] = np.asarray(self.all_G['dxdr']) self.all_G['seq'] = np.asarray(self.all_G['seq']) if self.stress: self.all_G['rdxdr'] = -np.asarray(self.all_G['rdxdr']) / vol else: self.all_G['rdxdr'] = None return self.all_G
def calculate(self, atoms=None, properties=['energy'], system_changes=all_changes): Calculator.calculate(self, atoms, properties, system_changes) natoms = len(self.atoms) sigma = self.parameters.sigma epsilon = self.parameters.epsilon tolerAngs = self.parameters.tolerAngs tolerMult = self.parameters.tolerMult cutoff = self.parameters.cutoff if cutoff is None: cutoff = 0.9 if 'numbers' in system_changes: self.nl = NeighborList([cutoff / 2] * natoms,\ self_interaction=False) self.nl.update(self.atoms) positions = self.atoms.positions cell = self.atoms.cell e0 = 4 * epsilon * ((sigma / cutoff)**12 - (sigma / cutoff)**6) energy = 0.0 forces = np.zeros((natoms, 3)) stress = np.zeros((3, 3)) def getLJparam(elem1, elem2): sigma_tmp = covalRadii[elem1] + covalRadii[elem1] return sigma_tmp for a1 in range(natoms): neighbors, offsets = self.nl.get_neighbors(a1) cells = np.dot(offsets, cell) d = positions[neighbors] + cells - positions[a1] r2 = (d**2).sum(1) covalBl = covalRadii[self.atoms.get_atomic_numbers()[a1]]+\ covalRadii[self.atoms.get_atomic_numbers()[neighbors]] c6 = (sigma**2 / r2)**3 c6[covalBl - np.sqrt(r2) < tolerAngs] = 0.0 c6[np.sqrt(r2) / covalBl > 1 - tolerMult] = 0.0 # c6[r2 > radius**2] = 0.0 energy -= e0 * (c6 != 0.0).sum() c12 = c6**2 energy += 4 * epsilon * (c12 - c6).sum() f = (24 * epsilon * (2 * c12 - c6) / r2)[:, np.newaxis] * d forces[a1] -= f.sum(axis=0) for a2, f2 in zip(neighbors, f): forces[a2] += f2 stress += np.dot(f.T, d) if 'stress' in properties: if self.atoms.number_of_lattice_vectors == 3: stress += stress.T.copy() stress *= -0.5 / self.atoms.get_volume() self.results['stress'] = stress.flat[[0, 4, 8, 5, 2, 1]] else: raise PropertyNotImplementedError self.results['energy'] = energy self.results['free_energy'] = energy self.results['forces'] = forces
class OPLSff: def __init__(self, fileobj=None, warnings=0): self.warnings = warnings self.data = {} if fileobj is not None: self.read(fileobj) def read(self, fileobj, comments='#'): if isinstance(fileobj, str): fileobj = open(fileobj) def read_block(name, symlen, nvalues): """Read a data block. name: name of the block to store in self.data symlen: length of the symbol nvalues: number of values expected """ if name not in self.data: self.data[name] = {} data = self.data[name] def add_line(): line = fileobj.readline().strip() if not len(line): # end of the block return False line = line.split('#')[0] # get rid of comments if len(line) > symlen: symbol = line[:symlen] words = line[symlen:].split() if len(words) >= nvalues: if nvalues == 1: data[symbol] = float(words[0]) else: data[symbol] = [ float(word) for word in words[:nvalues] ] return True while add_line(): pass read_block('one', 2, 3) read_block('bonds', 5, 2) read_block('angles', 8, 2) read_block('dihedrals', 11, 4) read_block('cutoffs', 5, 1) self.bonds = BondData(self.data['bonds']) self.angles = AnglesData(self.data['angles']) self.dihedrals = DihedralsData(self.data['dihedrals']) self.cutoffs = CutoffList(self.data['cutoffs']) def write_lammps(self, atoms, prefix='lammps'): """Write input for a LAMMPS calculation.""" self.prefix = prefix if hasattr(atoms, 'connectivities'): connectivities = atoms.connectivities else: btypes, blist = self.get_bonds(atoms) atypes, alist = self.get_angles() dtypes, dlist = self.get_dihedrals(alist, atypes) connectivities = { 'bonds': blist, 'bond types': btypes, 'angles': alist, 'angle types': atypes, 'dihedrals': dlist, 'dihedral types': dtypes } self.write_lammps_definitions(atoms, btypes, atypes, dtypes) self.write_lammps_in() self.write_lammps_atoms(atoms, connectivities) def write_lammps_in(self): fileobj = self.prefix + '_in' if isinstance(fileobj, str): fileobj = open(fileobj, 'w') fileobj.write("""# LAMMPS relaxation (written by ASE) units metal atom_style full boundary p p p #boundary p p f """) fileobj.write('read_data ' + self.prefix + '_atoms\n') fileobj.write('include ' + self.prefix + '_opls\n') fileobj.write(""" kspace_style pppm 1e-5 #kspace_modify slab 3.0 neighbor 1.0 bin neigh_modify delay 0 every 1 check yes thermo 1000 thermo_style custom step temp press cpu pxx pyy pzz pxy pxz pyz ke pe etotal vol lx ly lz atoms dump 1 all xyz 1000 dump_relax.xyz dump_modify 1 sort id restart 100000 test_relax min_style fire minimize 1.0e-14 1.0e-5 100000 100000 """) fileobj.close() def write_lammps_atoms(self, atoms, connectivities): """Write atoms input for LAMMPS""" fname = self.prefix + '_atoms' fileobj = open(fname, 'w') # header fileobj.write(fileobj.name + ' (by ' + str(self.__class__) + ')\n\n') fileobj.write(str(len(atoms)) + ' atoms\n') fileobj.write(str(len(atoms.types)) + ' atom types\n') blist = connectivities['bonds'] if len(blist): btypes = connectivities['bond types'] fileobj.write(str(len(blist)) + ' bonds\n') fileobj.write(str(len(btypes)) + ' bond types\n') alist = connectivities['angles'] if len(alist): atypes = connectivities['angle types'] fileobj.write(str(len(alist)) + ' angles\n') fileobj.write(str(len(atypes)) + ' angle types\n') dlist = connectivities['dihedrals'] if len(dlist): dtypes = connectivities['dihedral types'] fileobj.write(str(len(dlist)) + ' dihedrals\n') fileobj.write(str(len(dtypes)) + ' dihedral types\n') # cell p = Prism(atoms.get_cell()) xhi, yhi, zhi, xy, xz, yz = p.get_lammps_prism_str() fileobj.write('\n0.0 %s xlo xhi\n' % xhi) fileobj.write('0.0 %s ylo yhi\n' % yhi) fileobj.write('0.0 %s zlo zhi\n' % zhi) # atoms fileobj.write('\nAtoms\n\n') tag = atoms.get_tags() if atoms.has('molid'): molid = atoms.get_array('molid') else: molid = [1] * len(atoms) for i, r in enumerate(p.positions_to_lammps_strs( atoms.get_positions())): atype = atoms.types[tag[i]] if len(atype) < 2: atype = atype + ' ' q = self.data['one'][atype][2] fileobj.write('%6d %3d %3d %s %s %s %s' % ((i + 1, molid[i], tag[i] + 1, q) + tuple(r))) fileobj.write(' # ' + atoms.types[tag[i]] + '\n') # velocities velocities = atoms.get_velocities() if velocities is not None: fileobj.write('\nVelocities\n\n') for i, v in enumerate(velocities): fileobj.write('%6d %g %g %g\n' % (i + 1, v[0], v[1], v[2])) # masses fileobj.write('\nMasses\n\n') for i, typ in enumerate(atoms.types): cs = atoms.split_symbol(typ)[0] fileobj.write( '%6d %g # %s -> %s\n' % (i + 1, atomic_masses[chemical_symbols.index(cs)], typ, cs)) # bonds if len(blist): fileobj.write('\nBonds\n\n') for ib, bvals in enumerate(blist): fileobj.write( '%8d %6d %6d %6d ' % (ib + 1, bvals[0] + 1, bvals[1] + 1, bvals[2] + 1)) try: fileobj.write('# ' + btypes[bvals[0]]) except: pass fileobj.write('\n') # angles if len(alist): fileobj.write('\nAngles\n\n') for ia, avals in enumerate(alist): fileobj.write('%8d %6d %6d %6d %6d ' % (ia + 1, avals[0] + 1, avals[1] + 1, avals[2] + 1, avals[3] + 1)) try: fileobj.write('# ' + atypes[avals[0]]) except: pass fileobj.write('\n') # dihedrals if len(dlist): fileobj.write('\nDihedrals\n\n') for i, dvals in enumerate(dlist): fileobj.write('%8d %6d %6d %6d %6d %6d ' % (i + 1, dvals[0] + 1, dvals[1] + 1, dvals[2] + 1, dvals[3] + 1, dvals[4] + 1)) try: fileobj.write('# ' + dtypes[dvals[0]]) except: pass fileobj.write('\n') def update_neighbor_list(self, atoms): cut = 0.5 * max(self.data['cutoffs'].values()) self.nl = NeighborList([cut] * len(atoms), skin=0, bothways=True, self_interaction=False) self.nl.update(atoms) self.atoms = atoms def get_bonds(self, atoms): """Find bonds and return them and their types""" cutoffs = CutoffList(self.data['cutoffs']) self.update_neighbor_list(atoms) types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() bond_list = [] bond_types = [] for i, atom in enumerate(atoms): iname = types[tags[i]] indices, offsets = self.nl.get_neighbors(i) for j, offset in zip(indices, offsets): if j <= i: continue # do not double count jname = types[tags[j]] cut = cutoffs.value(iname, jname) if cut is None: if self.warnings > 1: print('Warning: cutoff %s-%s not found' % (iname, jname)) continue # don't have it dist = np.linalg.norm(atom.position - atoms[j].position - np.dot(offset, cell)) if dist > cut: continue # too far away name, val = self.bonds.name_value(iname, jname) if name is None: if self.warnings: print('Warning: potential %s-%s not found' % (iname, jname)) continue # don't have it if name not in bond_types: bond_types.append(name) bond_list.append([bond_types.index(name), i, j]) return bond_types, bond_list def get_angles(self, atoms=None): cutoffs = CutoffList(self.data['cutoffs']) if atoms is not None: self.update_neighbor_list(atoms) else: atoms = self.atoms types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() ang_list = [] ang_types = [] # center atom *-i-* for i, atom in enumerate(atoms): iname = types[tags[i]] indicesi, offsetsi = self.nl.get_neighbors(i) # search for first neighbor j-i-* for j, offsetj in zip(indicesi, offsetsi): jname = types[tags[j]] cut = cutoffs.value(iname, jname) if cut is None: continue # don't have it dist = np.linalg.norm(atom.position - atoms[j].position - np.dot(offsetj, cell)) if dist > cut: continue # too far away # search for second neighbor j-i-k for k, offsetk in zip(indicesi, offsetsi): if k <= j: continue # avoid double count kname = types[tags[k]] cut = cutoffs.value(iname, kname) if cut is None: continue # don't have it dist = np.linalg.norm(atom.position - np.dot(offsetk, cell) - atoms[k].position) if dist > cut: continue # too far away name, val = self.angles.name_value(jname, iname, kname) if name is None: if self.warnings > 1: print('Warning: angles %s-%s-%s not found' % (jname, iname, kname)) continue # don't have it if name not in ang_types: ang_types.append(name) ang_list.append([ang_types.index(name), j, i, k]) return ang_types, ang_list def get_dihedrals(self, ang_types, ang_list): 'Dihedrals derived from angles.' cutoffs = CutoffList(self.data['cutoffs']) atoms = self.atoms types = atoms.get_types() tags = atoms.get_tags() cell = atoms.get_cell() dih_list = [] dih_types = [] def append(name, i, j, k, l): if name not in dih_types: dih_types.append(name) index = dih_types.index(name) if (([index, i, j, k, l] not in dih_list) and ([index, l, k, j, i] not in dih_list)): dih_list.append([index, i, j, k, l]) for angle in ang_types: l, i, j, k = angle iname = types[tags[i]] jname = types[tags[j]] kname = types[tags[k]] # search for l-i-j-k indicesi, offsetsi = self.nl.get_neighbors(i) for l, offsetl in zip(indicesi, offsetsi): if l == j: continue # avoid double count lname = types[tags[l]] cut = cutoffs.value(iname, lname) if cut is None: continue # don't have it dist = np.linalg.norm(atoms[i].position - atoms[l].position - np.dot(offsetl, cell)) if dist > cut: continue # too far away name, val = self.dihedrals.name_value(lname, iname, jname, kname) if name is None: continue # don't have it append(name, l, i, j, k) # search for i-j-k-l indicesk, offsetsk = self.nl.get_neighbors(k) for l, offsetl in zip(indicesk, offsetsk): if l == j: continue # avoid double count lname = types[tags[l]] cut = cutoffs.value(kname, lname) if cut is None: continue # don't have it dist = np.linalg.norm(atoms[k].position - atoms[l].position - np.dot(offsetl, cell)) if dist > cut: continue # too far away name, val = self.dihedrals.name_value(iname, jname, kname, lname) if name is None: continue # don't have it append(name, i, j, k, l) return dih_types, dih_list def write_lammps_definitions(self, atoms, btypes, atypes, dtypes): """Write force field definitions for LAMMPS.""" fileobj = self.prefix + '_opls' if isinstance(fileobj, str): fileobj = open(fileobj, 'w') fileobj.write('# OPLS potential\n') fileobj.write('# write_lammps' + str(time.asctime(time.localtime(time.time())))) # bonds if len(btypes): fileobj.write('\n# bonds\n') fileobj.write('bond_style harmonic\n') for ib, btype in enumerate(btypes): fileobj.write('bond_coeff %6d' % (ib + 1)) for value in self.bonds.nvh[btype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + btype + '\n') # angles if len(atypes): fileobj.write('\n# angles\n') fileobj.write('angle_style harmonic\n') for ia, atype in enumerate(atypes): fileobj.write('angle_coeff %6d' % (ia + 1)) for value in self.angles.nvh[atype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + atype + '\n') # dihedrals if len(dtypes): fileobj.write('\n# dihedrals\n') fileobj.write('dihedral_style opls\n') for i, dtype in enumerate(dtypes): fileobj.write('dihedral_coeff %6d' % (i + 1)) for value in self.dihedrals.nvh[dtype]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + dtype + '\n') # Lennard Jones settings fileobj.write('\n# L-J parameters\n') fileobj.write('pair_style lj/cut/coul/long 10.0 7.4' + ' # consider changing these parameters\n') fileobj.write('special_bonds lj/coul 0.0 0.0 0.5\n') data = self.data['one'] for ia, atype in enumerate(atoms.types): if len(atype) < 2: atype = atype + ' ' fileobj.write('pair_coeff ' + str(ia + 1) + ' ' + str(ia + 1)) for value in data[atype][:2]: fileobj.write(' ' + str(value)) fileobj.write(' # ' + atype + '\n') fileobj.write('pair_modify shift yes mix geometric\n') # Charges fileobj.write('\n# charges\n') for ia, atype in enumerate(atoms.types): if len(atype) < 2: atype = atype + ' ' fileobj.write('set type ' + str(ia + 1)) fileobj.write(' charge ' + str(data[atype][2])) fileobj.write(' # ' + atype + '\n')
def get_bond_matrix(sbu): """Guesses the bond order in neighbourlist based on covalent radii the radii for BO > 1 are extrapolated by removing 0.1 Angstroms by order see Beatriz Cordero, Veronica Gomez, Ana E. Platero-Prats, Marc Reves, Jorge Echeverria, Eduard Cremades, Flavia Barragan and Santiago Alvarez (2008). "Covalent radii revisited". Dalton Trans. (21): 2832-2838 http://dx.doi.org/10.1039/b801115j """ # first guess bonds = numpy.zeros((len(sbu), len(sbu))) symbols = numpy.array(sbu.get_chemical_symbols()) numbers = numpy.array(sbu.get_atomic_numbers()) positions = numpy.array(sbu.get_positions()) BO1 = numpy.array([covalent_radii[n] if n > 0 else 0.35 for n in numbers]) BO2 = BO1 - 0.15 BO3 = BO2 - 0.15 nl1 = NeighborList(cutoffs=BO1, bothways=True, self_interaction=False, skin=0.1) nl2 = NeighborList(cutoffs=BO2, bothways=True, self_interaction=False, skin=0.1) nl3 = NeighborList(cutoffs=BO3, bothways=True, self_interaction=False, skin=0.1) nl1.update(sbu) nl2.update(sbu) nl3.update(sbu) for atom in sbu: i1, _ = nl1.get_neighbors(atom.index) i2, _ = nl2.get_neighbors(atom.index) i3, _ = nl3.get_neighbors(atom.index) bonds[atom.index, i1] = 1.0 bonds[atom.index, i2] = 2.0 bonds[atom.index, i3] = 3.0 # cleanup with particular cases # identify particular atoms hydrogens = numpy.where(symbols == "H")[0] metals = numpy.where(is_metal(symbols))[0] alkali = numpy.where(is_alkali(symbols))[0] # the rest is dubbed "organic" organic = numpy.ones(bonds.shape) organic[hydrogens, :] = False organic[metals, :] = False organic[alkali, :] = False organic[:, hydrogens] = False organic[:, metals] = False organic[:, alkali] = False organic = numpy.where(organic)[0] # Hydrogen has BO of 1 bonds_h = bonds[hydrogens] bonds_h[bonds_h > 1.0] = 1.0 bonds[hydrogens, :] = bonds_h bonds[:, hydrogens] = bonds_h.T #Metal-Metal bonds: if no special case, nominal bond ix = numpy.ix_(metals, metals) bix = bonds[ix] bix[numpy.nonzero(bix)] = 0.25 bonds[ix] = bix # no H-Metal bonds ix = numpy.ix_(metals, hydrogens) bonds[ix] = 0.0 ix = numpy.ix_(hydrogens, metals) bonds[ix] = 0.0 # no alkali-alkali bonds ix = numpy.ix_(alkali, alkali) bonds[ix] = 0.0 # no alkali-metal bonds ix = numpy.ix_(metals, alkali) bonds[ix] = 0.0 ix = numpy.ix_(alkali, metals) bonds[ix] = 0.0 # metal-organic is coordination bond ix = numpy.ix_(metals, organic) bix = bonds[ix] bix[numpy.nonzero(bix)] = 0.5 bonds[ix] = bix ix = numpy.ix_(organic, metals) bix = bonds[ix] bix[numpy.nonzero(bix)] = 0.5 bonds[ix] = bix # aromaticity and rings rings = [] # first, use the compressed sparse graph object # we only care about organic bonds and not hydrogens graph_bonds = numpy.array(bonds > 0.99, dtype=float) graph_bonds[hydrogens, :] = 0.0 graph_bonds[:, hydrogens] = 0.0 graph = csgraph.csgraph_from_dense(graph_bonds) for sg in graph.indices: subgraph = graph[sg] for i, j in combinations(subgraph.indices, 2): t0 = csgraph.breadth_first_tree(graph, i_start=i, directed=False) t1 = csgraph.breadth_first_tree(graph, i_start=j, directed=False) t0i = t0.indices t1i = t1.indices ring = sorted(set(list(t0i) + list(t1i) + [i, j, sg])) # some conditions seen = (ring in rings) isring = (sorted(t0i[1:]) == sorted(t1i[1:])) bigenough = (len(ring) >= 5) smallenough = (len(ring) <= 10) if isring and not seen and bigenough and smallenough: rings.append(ring) # we now have a list of all the shortest rings within # the molecular graph. If planar, the ring might be aromatic aromatic_epsilon = 0.1 aromatic = [] for ring in rings: homocycle = (symbols[ring] == "C").all() heterocycle = numpy.in1d(symbols[ring], numpy.array(["C", "S", "N", "O"])).all() if (homocycle and (len(ring) % 2) == 0) or heterocycle: ring_positions = positions[ring] # small function for coplanarity coplanar = all([ numpy.linalg.det(numpy.array(x[:3]) - x[3]) < aromatic_epsilon for x in combinations(ring_positions, 4) ]) if coplanar: aromatic.append(ring) # aromatic bond fixing aromatic = numpy.array(aromatic).ravel() ix = numpy.ix_(aromatic, aromatic) bix = bonds[ix] bix[numpy.nonzero(bix)] = 1.5 bonds[ix] = bix # hydrogen bonds # TODO return bonds
def do_one_vacancy(bulk_supercell, calculator, relax_radial=0.0, relax_symm_break=0.0, nn_cutoff=0.0, tol=1.0e-2): bulk_supercell_pe = bulk_supercell.get_potential_energy() vac_i = 0 # do unrelaxed (without perturbations) vac = bulk_supercell.copy() del vac[vac_i] label = "ind_%d_Z_%d" % (vac_i, bulk_supercell.get_atomic_numbers()[vac_i]) unrelaxed_filename = "-%s-unrelaxed.xyz" % label ase.io.write(os.path.join("..", unrelaxed_filename), vac, format='extxyz') #evaluate(vac) vac.set_calculator(calculator) unrelaxed_vac_pe = vac.get_potential_energy() # recreate with perturbations for relaxation vac = bulk_supercell.copy() if relax_radial != 0.0 or relax_symm_break != 0.0: nl = NeighborList([nn_cutoff / 2.0] * len(bulk_supercell), self_interaction=False, bothways=True) nl.update(bulk_supercell) indices, offsets = nl.get_neighbors(vac_i) offset_factor = relax_radial for i, offset in zip(indices, offsets): ri = vac.positions[vac_i] - (vac.positions[i] + np.dot(offset, vac.get_cell())) vac.positions[i] += offset_factor * ri offset_factor += relax_symm_break del vac[vac_i] vac_pos = vac.positions[vac_i] vac = relax_config(vac, calculator, relax_pos=True, relax_cell=False, tol=tol, traj_file=None, config_label=label, from_base_model=True, save_config=True) relaxed_filename = "-%s-relaxed.xyz" % label ase.io.write(os.path.join("..", relaxed_filename), vac, format='extxyz') # already has calculator from relax_configs vac_pe = vac.get_potential_energy() if len(set(bulk_supercell.get_atomic_numbers())) == 1: Ebulk = float(len(vac)) / float( len(bulk_supercell)) * bulk_supercell_pe else: Ebulk = bulk_supercell_pe Ef0 = unrelaxed_vac_pe - Ebulk Ef = vac_pe - Ebulk print("got vacancy", label, "cell energy", vac_pe, "n_atoms", len(vac)) print("got bulk energy", Ebulk, " (scaled to (N-1)/N if single component)") return (label, unrelaxed_filename, Ef0, relaxed_filename, Ef, int(bulk_supercell.get_atomic_numbers()[vac_i]), vac_pos)
def build_neighbor_list(self): ''' Builds a neighborlist for the calculation of bispectrum components for a given ASE atoms object given in the calculate method. ''' if self._backend == 'ase': atoms = self._atoms # cutoffs for each atom cutoffs = [self.rcut / 2] * len(atoms) # instantiate neighborlist calculator nl = NeighborList(cutoffs, self_interaction=False, bothways=True, skin=0.0) # provide atoms object to neighborlist calculator nl.update(atoms) # instantiate memory for neighbor separation vectors, periodic indices, and atomic numbers center_atoms = np.zeros((len(atoms), 3), dtype=np.float64) neighbors = [] neighbor_indices = [] atomic_numbers = [] max_len = 0 for i in range(len(atoms)): # get center atom position center_atom = atoms.positions[i] center_atoms[i] = center_atom # get indices and cell offsets of each neighbor indices, offsets = nl.get_neighbors(i) # add an empty list to neighbors and atomic numbers for population neighbors.append([]) atomic_numbers.append([]) # the indices are already numpy arrays so just append as is neighbor_indices.append(indices) for j, offset in zip(indices, offsets): # compute separation vector pos = atoms.positions[j] + np.dot(offset, atoms.get_cell()) - center_atom neighbors[i].append(pos) atomic_numbers[i].append(atoms[j].number) if len(neighbors[i]) > max_len: max_len = len(neighbors[i]) # declare arrays to store the separation vectors, neighbor indices # atomic numbers of each neighbor, and the atomic numbers of each # site neighborlist = np.zeros((len(atoms), max_len, 3), dtype=np.float64) neighbor_inds = np.zeros((len(atoms), max_len), dtype=np.int64) atm_nums = np.zeros((len(atoms), max_len), dtype=np.int64) site_atomic_numbers = np.array(list(atoms.numbers), dtype=np.int64) # populate the arrays with list elements for i in range(len(atoms)): neighborlist[i, :len(neighbors[i]), :] = neighbors[i] neighbor_inds[i, :len(neighbors[i])] = neighbor_indices[i] atm_nums[i, :len(neighbors[i])] = atomic_numbers[i] elif self._backend == 'pymatgen': from pymatgen.io.ase import AseAtomsAdaptor struc = AseAtomsAdaptor.get_structure(self._atoms) neighbors = struc.get_all_neighbors(self._rcut, include_index=True) max_len = 0 for i, neighlist in enumerate(neighbors): if len(neighlist) > max_len: max_len = len(neighlist) center_atoms = np.zeros((len(struc), 3), dtype=np.float64) neighborlist = np.zeros((len(struc), max_len, 3), dtype=np.float64) neighbor_inds = np.zeros((len(struc), max_len), dtype=np.int64) atm_nums = np.zeros((len(struc), max_len), dtype=np.int64) site_atomic_numbers = np.zeros(len(struc), dtype=np.int64) for i, site in enumerate(struc): neighlist = neighbors[i] site_atomic_numbers[i] = site.specie.number center_atoms[i] = site.coords for j, neighbor in enumerate(neighlist): neighborlist[i, j, :] = neighbor[0].coords - site.coords neighbor_inds[i, j] = neighbor[2] atm_nums[i, j] = neighbor[0].specie.number else: raise NotImplementedError('Specified backend not supported') # assign these arrays to attributes self.center_atoms = center_atoms self.neighborlist = neighborlist self.neighbor_indices = neighbor_inds self.atomic_numbers = atm_nums self.site_atomic_numbers = site_atomic_numbers return
def update_neighbor_list(self, atoms): cut = 0.5 * max(self.data['cutoffs'].values()) self.nl = NeighborList([cut] * len(atoms), skin=0, bothways=True, self_interaction=False) self.nl.update(atoms) self.atoms = atoms
from ase.io import read, write from ase.neighborlist import NeighborList from ase.neighborlist import natural_cutoffs nt = [0]*32 atoms = read('run-01.pdb') nl = NeighborList(natural_cutoffs(atoms, 1.35)) nl.update(atoms) o = open('nl.dat', 'w') for i in range(len(atoms)): indices, offsets = nl.get_neighbors(i) nt[len(indices)] += 1 o.write('%d\n'%len(indices))
def get_all_interstitials(ats, unique_atoms): """Function to return list of all interstitial sites using Voronoi.py Args: ats(:ase:class:`Atoms`):Atoms object of structure. positions(numpy array): Lattice sites to compute interstitials for. Returns: list of list, with inner list containing ['Interstitial Type', [x,y,z]] Interstitial type can be of type 'B', 'C', 'N' corresponding to; 'B' Voronoi vertices. 'C' face centers. 'N' Edge centers. """ ints_list = [] #generate neighbours list. for site_num in unique_atoms: nl = NeighborList([2.0] * len(ats), bothways=False, self_interaction=True) nl.update(ats) site = np.array([ ats[site_num].position[0], ats[site_num].position[1], ats[site_num].position[2] ]) points = [] indices, offsets = nl.get_neighbors(site_num) print 'site_num:', site_num for i, offset in zip(indices, offsets): if offset[2] == 0.0: print 'neighb:', i, ats.positions[i], offset, np.dot( offset, ats.get_cell()) points.append(ats.positions[i] + np.dot(offset, ats.get_cell())) ### converting list to numpy array print "neighbors", len(points) points = np.asarray(points) voronoi = Voronoi() ### using tess object cntr to compute voronoi cntr = voronoi.compute_voronoi(points) ### Voronoi vertices ### the first position in points is the site, therefore '0' v = voronoi.get_vertices(0, cntr) for i in range(len(v)): ints_list.append(['B', v[i].tolist()]) ### Voronoi face centers f = voronoi.get_facecentroid(0, cntr) for j in range(len(f)): ints_list.append(['C', f[j].tolist()]) ### Voronoi edge centers e = voronoi.get_edgecenter(0, cntr) for k in range(len(e)): ints_list.append(['N', e[k].tolist()]) ### return list of list ['Atom type', [x,y,z]] return ints_list
def CoreShellFCC(atoms, type_a, type_b, ratio, a_cell, n_depth=-1): r"""This routine generates cluster with ideal core-shell architecture, so that atoms of type_a are placed on the surface and atoms of type_b are forming the core of nanoparticle. The 'surface' of nanoparticle is defined as atoms with unfinished coordination shell. Parameters ---------- atoms: ase.Atoms ase Atoms object, containing atomic cluster. type_a: string Symbol of chemical element to be placed on the shell. type_b: string Symbol of chemical element to be placed in the core. ratio: float Guards the number of shell atoms, type_a:type_b = ratio:(1-ratio) a_cell: float Parameter of FCC cell, in Angstrom. Required for calculation of neighbor distances in for infinite crystal. n_depth: int Number of layers of the shell formed by atoms ratio. Default value -1 is ignored and n_depth is calculated according ratio. If n_depth is set then value of ratio is ignored. Returns ------- Function returns ASE atoms object which contains bimetallic core-shell cluster Notes ----- The criterion of the atom beeing on the surface is incompletnes of it's coordination shell. For the most outer atoms the first coordination shell will be incomplete (coordination number is less then 12 for FCC), for the second layer -- second coordination shell( CN1 + CN2 < 12 + 6) and so on. In this algorithm each layer is tagged by the number ('depth'), take care if used with other routines dealing with tags (add_adsorbate etc). First, atoms with unfinished first shell are replaced by atoms type_a, then second, and so on. The last depth surface layer is replaced by random to maintain given ratio value. Example -------- >>> atoms = FaceCenteredCubic('Ag', [(1, 0, 0), (1, 1, 0), (1, 1, 1)], [7,8,7], 4.09) >>> atoms = CoreShellFCC(atoms, 'Pt', 'Ag', 0.6, 4.09) >>> view(atoms) """ # 0 < ratio < 1 target_x = ratio if n_depth != -1: target_x = 1 # needed to label all needed layeres def fill_by_tag(atoms, chems, tag): """Replaces all atoms within selected layer""" for i in xrange(0, len(atoms)): if atoms[i].tag == tag: chems[i] = type_a return # coord numbers for FCC: coord_nums = [1, 12, 6, 24, 12, 24, 8, 48, 6, 36, 24, 24, 24, 72, 48, 12, 48, 30, 72, 24] # coordination radii obtained from this array as R = sqrt(coord_radii)*a/2 coord_radii = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 30, 32, 34, 36, 38, 40] ## generate FCC cluster ## #atoms = FaceCenteredCubic(type_b, surfaces, layers, a_cell) n_atoms = len(atoms) ## tag layers ## positions = [0] # number of positions in layer n_tag = 0 # number of tags to check if there is enought layers n_shell = 0 # depth of the shell while (n_tag < n_atoms * target_x): n_shell += 1 if (n_depth != -1)and(n_shell > n_depth): break neiblist = NeighborList( [ a_cell / 2.0 * sqrt(coord_radii[n_shell]) / 2.0 + 0.0001 ] * n_atoms, self_interaction=False, bothways=True ) neiblist.build(atoms) for i in xrange(0, n_atoms): indeces, offsets = neiblist.get_neighbors(i) if (atoms[i].tag == 0): if (len(indeces) < sum(coord_nums[1:n_shell + 1])): # coord shell is not full -> atom is on surface! atoms[i].tag = n_shell n_tag += 1 # save the count of positions at each layer: positions.append(n_tag - sum(positions[0:n_shell])) ## populate layers ## chems = atoms.get_chemical_symbols() n_type_a = 0 # number of changes B -> A if (n_tag < n_atoms * target_x)and(n_depth == -1): # raise exception? return None else: n_filled = n_shell - 1 # number of totally filled layers ilayer = 1 while (ilayer < n_filled + 1): fill_by_tag(atoms, chems, ilayer) n_type_a += positions[ilayer] ilayer += 1 while (n_type_a < n_atoms * target_x)and(n_depth == -1): i = random.randint(0, n_atoms - 1) if (atoms[i].tag == n_shell): if (chems[i] == type_b): chems[i] = type_a n_type_a += 1 atoms.set_chemical_symbols(chems) ## check number of atoms ## checkn_a = 0 for element in chems: if element == type_a: checkn_a += 1 assert n_type_a == checkn_a return atoms
class RepulsivePotential(Calculator): """ ASE Calculator representing the repulsive potentials in a DFTB parameter set. Arguments: atoms: an ASE atoms object to which the calculator will be attached skfdict: dict with the (paths to the) SKF files containing the (exponential+spline-based) repulsive potentials, one for every (alphabetically sorted) element pair, e.g.: {'O-O':'O-O.skf', 'H-O':'H-O.skf', 'H-H':'H-H.skf'}. If equal to None, all files are assumed to reside in the $DFTB_PREFIX folder and formatted as *-*.skf as in the example above. """ implemented_properties = ['energy', 'forces'] def __init__(self, atoms, skfdict=None): Calculator.__init__(self) elements = np.unique(atoms.get_chemical_symbols()) self.pairs = get_skf_prefixes(elements, redundant=False) rcut = 0. self.func = {} for p in self.pairs: if skfdict is None: f = os.environ['DFTB_PREFIX'] + '/%s.skf' % p else: assert p in skfdict, 'No SKF file specified for %s' % p f = skfdict[p] assert os.path.exists(f), 'SKF file %s does not exist' % f self.func[p] = read_spline_from_skf(f) rcut = max([rcut, self.func[p].rcut]) self.nl = NeighborList([rcut * Bohr / 2.] * len(atoms), skin=1., bothways=False, self_interaction=False) def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) N = len(atoms) energy, forces = 0, np.zeros((N, 3)) cell = atoms.get_cell() sym = atoms.get_chemical_symbols() pos = atoms.get_positions() self.nl.update(atoms) for i in range(N): indices, offsets = self.nl.get_neighbors(i) p = pos[indices] + np.dot(offsets, cell) r = cdist(p, [pos[i]]) v = pos[i] - p for j, index in enumerate(indices): p = '-'.join(sorted([sym[i], sym[index]])) d = r[j][0] energy += self.func[p](d, der=0) f = self.func[p](d, der=1) * v[j] / d forces[index] += f forces[i] -= f self.results = {'energy': energy, 'forces': forces}
def get_neighbours(atoms, r_cut, self_interaction=False): """Return a list of pairs of atoms within a given distance of each other. If matscipy can be imported, then this will directly call matscipy's neighbourlist function. Otherwise it will use ASE's NeighborList object. Args: atoms: ase.atoms object to calculate neighbours for r_cut: cutoff radius (float). Pairs of atoms are considered neighbours if they are within a distance r_cut of each other (note that this is double the parameter used in the ASE's neighborlist module) Returns: a tuple (i_list, j_list, d_list, fixed_atoms): i_list, j_list: i and j indices of each neighbour pair d_list: absolute distance between the corresponding pair fixed_atoms: indices of any fixed atoms """ if isinstance(atoms, Filter): atoms = atoms.atoms if have_matscipy: i_list, j_list, d_list = neighbour_list('ijd', atoms, r_cut) else: radii = [r_cut / 2 for i in range(len(atoms))] nl = NeighborList(radii, sorted=False, self_interaction=False, bothways=True) nl.update(atoms) i_list = [] j_list = [] d_list = [] for i, atom in enumerate(atoms): posn_i = atom.position indices, offsets = nl.get_neighbors(i) assert len(indices) == len(offsets) for j, offset in zip(indices, offsets): # Offsets represent how far away an atom is from its pair in terms # of the repeating cell - for instance, an atom i might be in cell # (0, 0, 0) while the neighbouring atom j is in cell (0, 1, 1). To # get the true position we have to correct for the offset: posn_j = atoms.positions[j] + np.dot(offset, atoms.get_cell()) distance = np.sqrt(((posn_j - posn_i)**2).sum()) i_list.append(i) j_list.append(j) d_list.append(distance) i_list = np.array(i_list) j_list = np.array(j_list) d_list = np.array(d_list) # filter out self-interactions (across PBC) if not self_interaction: mask = i_list != j_list i_list = i_list[mask] j_list = j_list[mask] d_list = d_list[mask] # filter out bonds where 1st atom (i) in pair is fixed fixed_atoms = [] for constraint in atoms.constraints: if isinstance(constraint, FixAtoms): fixed_atoms.extend(list(constraint.index)) else: raise TypeError( 'only FixAtoms constraints are supported by Precon class') return i_list, j_list, d_list, fixed_atoms