def test_first_neighbours(self): i = [1, 1, 1, 1, 3, 3, 3] self.assertArrayAlmostEqual(first_neighbours(5, i), [-1, 0, -1, 4, -1, 7]) i = [0, 1, 2, 3, 4, 5] self.assertArrayAlmostEqual(first_neighbours(6, i), [0, 1, 2, 3, 4, 5, 6])
def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) # construct neighbor list i_p, j_p, r_p, r_pc = neighbour_list('ijdD', atoms=atoms, cutoff=self.cutoff) nb_atoms = len(self.atoms) nb_pairs = len(i_p) # normal vectors n_pc = (r_pc.T / r_p).T nx_p, ny_p, nz_p = n_pc.T # construct triplet list first_i = first_neighbours(nb_atoms, i_p) ij_t, ik_t = triplet_list(first_i) # calculate energy G_t = self.G(r_pc[ij_t], r_pc[ik_t]) xi_p = np.bincount(ij_t, weights=G_t, minlength=nb_pairs) F_p = self.F(r_p, xi_p) epot = 0.5 * np.sum(F_p) d1G_t = self.d1G(r_pc[ij_t], r_pc[ik_t]) d2F_d2G_t = (self.d2F(r_p[ij_t], xi_p[ij_t]) * self.d2G(r_pc[ij_t], r_pc[ik_t]).T).T # calculate forces (per pair) fx_p = \ self.d1F(r_p, xi_p) * n_pc[:, 0] + \ self.d2F(r_p, xi_p) * np.bincount(ij_t, d1G_t[:, 0], minlength=nb_pairs) + \ np.bincount(ik_t, d2F_d2G_t[:, 0], minlength=nb_pairs) fy_p = \ self.d1F(r_p, xi_p) * n_pc[:, 1] + \ self.d2F(r_p, xi_p) * np.bincount(ij_t, d1G_t[:, 1], minlength=nb_pairs) + \ np.bincount(ik_t, d2F_d2G_t[:, 1], minlength=nb_pairs) fz_p = \ self.d1F(r_p, xi_p) * n_pc[:, 2] + \ self.d2F(r_p, xi_p) * np.bincount(ij_t, d1G_t[:, 2], minlength=nb_pairs) + \ np.bincount(ik_t, d2F_d2G_t[:, 2], minlength=nb_pairs) # collect atomic forces fx_n = 0.5 * (np.bincount(i_p, weights=fx_p) - np.bincount(j_p, weights=fx_p)) fy_n = 0.5 * (np.bincount(i_p, weights=fy_p) - np.bincount(j_p, weights=fy_p)) fz_n = 0.5 * (np.bincount(i_p, weights=fz_p) - np.bincount(j_p, weights=fz_p)) f_n = np.transpose([fx_n, fy_n, fz_n]) self.results = {'energy': epot, 'forces': f_n}
def hydrogenate(a, cutoff, bond_length, b=None, mask=[True, True, True], exclude=None, vacuum=None): """ Hydrogenate a slab of material at its periodic boundary conditions. Boundary conditions are turned into nonperiodic. Parameters ---------- a : ase.Atoms Atomic configuration. cutoff : float Cutoff for neighbor counting. bond_length : float X-H bond length for hydrogenation. b : ase.Atoms, optional If present, this is the configuration to hydrogenate. Number of atoms must be identical to a object. All bonds present in a but not present in b will be hydrogenated in b. mask : list of bool Cartesian directions which to hydrogenate, only if b argument is not given. exclude : array_like Boolean array masking atoms to be excluded from hydrogenation. vacuum : float, optional Add this much vacuum after hydrogenation. Returns ------- a : ase.Atoms Atomic configuration of the hydrogenated slab. """ if b is None: b = a.copy() b.set_pbc(np.logical_not(mask)) if exclude is None: exclude = np.zeros(len(a), dtype=bool) i_a, j_a, D_a, d_a = neighbour_list('ijDd', a, cutoff) i_b, j_b = neighbour_list('ij', b, cutoff) firstneigh_a = first_neighbours(len(a), i_a) firstneigh_b = first_neighbours(len(b), i_b) coord_a = np.bincount(i_a, minlength=len(a)) coord_b = np.bincount(i_b, minlength=len(b)) hydrogens = [] # Surface atoms have coord_a != coord_b. Those need hydrogenation for k in np.arange(len(a))[np.logical_and(coord_a != coord_b, np.logical_not(exclude))]: l1_a = firstneigh_a[k] l2_a = firstneigh_a[k + 1] l1_b = firstneigh_b[k] l2_b = firstneigh_b[k + 1] n_H = 0 for l_a in range(l1_a, l2_a): assert i_a[l_a] == k bond_exists = False for l_b in range(l1_b, l2_b): assert i_b[l_b] == k if j_a[l_a] == j_b[l_b]: bond_exists = True if not bond_exists: # Bond existed before cut hydrogens += [ b[k].position + bond_length * D_a[l_a] / d_a[l_a] ] n_H += 1 assert n_H == coord_a[k] - coord_b[k] if hydrogens == []: raise RuntimeError('No Hydrogen created.') b += ase.Atoms(['H'] * len(hydrogens), hydrogens) if vacuum is not None: axis = [] for i in range(3): if mask[i]: axis += [i] b.center(vacuum, axis=axis) return b
def hessian_matrix(self, atoms, divide_by_masses=False): """ Calculate the Hessian matrix for a polydisperse systems where atoms interact via a pair potential. For an atomic configuration with N atoms in d dimensions the hessian matrix is a symmetric, hermitian matrix with a shape of (d*N,d*N). The matrix is due to the cutoff function a sparse matrix, which consists of dense blocks of shape (d,d), which are the mixed second derivatives. The result of the derivation for a pair potential can be found in: L. Pastewka et. al. "Seamless elastic boundaries for atomistic calculations", Phys. Ev. B 86, 075459 (2012). Parameters ---------- atoms: ase.Atoms Atomic configuration in a local or global minima. divide_by_masses: bool Divide the block "l,m" by the corresponding atomic masses "sqrt(m_l, m_m)" to obtain dynamical matrix. Restrictions ---------- This method is currently only implemented for three dimensional systems """ if self.atoms is None: self.atoms = atoms f = self.f nat = len(self.atoms) if atoms.has("size"): size = self.atoms.get_array("size") else: raise AttributeError( "Attribute error: Unable to load atom sizes from atoms object! Probably missing size array." ) i_n, j_n, dr_nc, abs_dr_n = neighbour_list( "ijDd", self.atoms, f.get_maxSize() * f.get_cutoff()) ijsize = f.mix_sizes(size[i_n], size[j_n]) # Mask neighbour list to consider only true neighbors mask = abs_dr_n <= f.get_cutoff() * ijsize i_n = i_n[mask] j_n = j_n[mask] dr_nc = dr_nc[mask] abs_dr_n = abs_dr_n[mask] ijsize = ijsize[mask] first_i = first_neighbours(nat, i_n) if divide_by_masses: mass_nat = self.atoms.get_masses() geom_mean_mass_n = np.sqrt(mass_nat[i_n] * mass_nat[j_n]) # Hessian de_n = f.first_derivative(abs_dr_n, ijsize) dde_n = f.second_derivative(abs_dr_n, ijsize) e_nc = (dr_nc.T / abs_dr_n).T H_ncc = -(dde_n * (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3)).T).T H_ncc += -(de_n / abs_dr_n * (np.eye(3, dtype=e_nc.dtype) - (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3))).T).T if divide_by_masses: H = bsr_matrix(((H_ncc.T / geom_mean_mass_n).T, j_n, first_i), shape=(3 * nat, 3 * nat)) else: H = bsr_matrix((H_ncc, j_n, first_i), shape=(3 * nat, 3 * nat)) Hdiag_icc = np.empty((nat, 3, 3)) for x in range(3): for y in range(3): Hdiag_icc[:, x, y] = - \ np.bincount(i_n, weights=H_ncc[:, x, y]) if divide_by_masses: H += bsr_matrix(((Hdiag_icc.T / mass_nat).T, np.arange(nat), np.arange(nat + 1)), shape=(3 * nat, 3 * nat)) else: H += bsr_matrix((Hdiag_icc, np.arange(nat), np.arange(nat + 1)), shape=(3 * nat, 3 * nat)) return H
def calculate_hessian_matrix(self, atoms, divide_by_masses=False): r"""Compute the Hessian matrix The Hessian matrix is the matrix of second derivatives of the potential energy :math:`\mathcal{V}_\mathrm{int}` with respect to coordinates, i.e.\ .. math:: \frac{\partial^2 \mathcal{V}_\mathrm{int}} {\partial r_{\nu{}i}\partial r_{\mu{}j}}, where the indices :math:`\mu` and :math:`\nu` refer to atoms and the indices :math:`i` and :math:`j` refer to the components of the position vector :math:`r_\nu` along the three spatial directions. The Hessian matrix has contributions from the pair potential and the embedding energy, .. math:: \frac{\partial^2 \mathcal{V}_\mathrm{int}}{\partial r_{\nu{}i}\partial r_{\mu{}j}} = \frac{\partial^2 \mathcal{V}_\mathrm{pair}}{ \partial r_{\nu i} \partial r_{\mu j}} + \frac{\partial^2 \mathcal{V}_\mathrm{embed}}{\partial r_{\nu i} \partial r_{\mu j}}. The contribution from the pair potential is .. math:: \frac{\partial^2 \mathcal{V}_\mathrm{pair}}{ \partial r_{\nu i} \partial r_{\mu j}} &= -\phi_{\nu\mu}'' \left( \frac{r_{\nu\mu i}}{r_{\nu\mu}} \frac{r_{\nu\mu j}}{r_{\nu\mu}} \right) -\frac{\phi_{\nu\mu}'}{r_{\nu\mu}}\left( \delta_{ij}- \frac{r_{\nu\mu i}}{r_{\nu\mu}} \frac{r_{\nu\mu j}}{r_{\nu\mu}} \right) \\ &+\delta_{\nu\mu}\sum_{\gamma\neq\nu}^{N} \phi_{\nu\gamma}'' \left( \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \right) +\delta_{\nu\mu}\sum_{\gamma\neq\nu}^{N}\frac{\phi_{\nu\gamma}'}{r_{\nu\gamma}}\left( \delta_{ij}- \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \right). The contribution of the embedding energy to the Hessian matrix is a sum of eight terms, .. math:: \frac{\mathcal{V}_\mathrm{embed}}{\partial r_{\mu j} \partial r_{\nu i}} &= T_1 + T_2 + T_3 + T_4 + T_5 + T_6 + T_7 + T_8 \\ T_1 &= \delta_{\nu\mu}U_\nu'' \sum_{\gamma\neq\nu}^{N}g_{\nu\gamma}'\frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \sum_{\gamma\neq\nu}^{N}g_{\nu\gamma}'\frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \\ T_2 &= -u_\nu''g_{\nu\mu}' \frac{r_{\nu\mu j}}{r_{\nu\mu}} \sum_{\gamma\neq\nu}^{N} G_{\nu\gamma}' \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \\ T_3 &= +u_\mu''g_{\mu\nu}' \frac{r_{\nu\mu i}}{r_{\nu\mu}} \sum_{\gamma\neq\mu}^{N} G_{\mu\gamma}' \frac{r_{\mu\gamma j}}{r_{\mu\gamma}} \\ T_4 &= -\left(u_\mu'g_{\mu\nu}'' + u_\nu'g_{\nu\mu}''\right) \left( \frac{r_{\nu\mu i}}{r_{\nu\mu}} \frac{r_{\nu\mu j}}{r_{\nu\mu}} \right)\\ T_5 &= \delta_{\nu\mu} \sum_{\gamma\neq\nu}^{N} \left(U_\gamma'g_{\gamma\nu}'' + U_\nu'g_{\nu\gamma}''\right) \left( \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \right) \\ T_6 &= -\left(U_\mu'g_{\mu\nu}' + U_\nu'g_{\nu\mu}'\right) \frac{1}{r_{\nu\mu}} \left( \delta_{ij}- \frac{r_{\nu\mu i}}{r_{\nu\mu}} \frac{r_{\nu\mu j}}{r_{\nu\mu}} \right) \\ T_7 &= \delta_{\nu\mu} \sum_{\gamma\neq\nu}^{N} \left(U_\gamma'g_{\gamma\nu}' + U_\nu'g_{\nu\gamma}'\right) \frac{1}{r_{\nu\gamma}} \left(\delta_{ij}- \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \right) \\ T_8 &= \sum_{\substack{\gamma\neq\nu \\ \gamma \neq \mu}}^{N} U_\gamma'' g_{\gamma\nu}'g_{\gamma\mu}' \frac{r_{\gamma\nu i}}{r_{\gamma\nu}} \frac{r_{\gamma\mu j}}{r_{\gamma\mu}} Parameters ---------- atoms : ase.Atoms divide_by_masses : bool Divide block :math:`\nu\mu` by :math:`m_\num_\mu` to obtain the dynamical matrix Returns ------- D : numpy.matrix Block Sparse Row matrix with the nonzero blocks Notes ----- Notation: * :math:`N` Number of atoms * :math:`\mathcal{V}_\mathrm{int}` Total potential energy * :math:`\mathcal{V}_\mathrm{pair}` Pair potential * :math:`\mathcal{V}_\mathrm{embed}` Embedding energy * :math:`r_{\nu{}i}` Component :math:`i` of the position vector of atom :math:`\nu` * :math:`r_{\nu\mu{}i} = r_{\mu{}i}-r_{\nu{}i}` * :math:`r_{\nu\mu{}}` Norm of :math:`r_{\nu\mu{}i}`, i.e.\ :math:`\left(r_{\nu\mu{}1}^2+r_{\nu\mu{}2}^2+r_{\nu\mu{}3}^2\right)^{1/2}` * :math:`\phi_{\nu\mu}(r_{\nu\mu{}})` Pair potential energy of atoms :math:`\nu` and :math:`\mu` * :math:`\rho_nu` Total electron density of atom :math:`\nu` * :math:`U_\nu(\rho_nu)` Embedding energy of atom :math:`\nu` * :math:`g_{\delta}\left(r_{\gamma\delta}\right) \equiv g_{\gamma\delta}` Contribution from atom :math:`\delta` to :math:`\rho_\gamma` * :math:`m_\nu` mass of atom :math:`\nu` """ nat = len(atoms) atnums = atoms.numbers atnums_in_system = set(atnums) for atnum in atnums_in_system: if atnum not in atnums: raise RuntimeError('Element with atomic number {} found, but ' 'this atomic number has no EAM ' 'parametrization'.format(atnum)) # i_n: index of the central atom # j_n: index of the neighbor atom # dr_nc: distance vector between the two # abs_dr_n: norm of distance vector # Variable name ending with _n indicate arrays that contain # one element for each pair in the neighbor list. Names ending # with _i indicate arrays containing one element for each atom. i_n, j_n, dr_nc, abs_dr_n = neighbour_list('ijDd', atoms, self._db_cutoff) first_i = first_neighbours(nat, i_n) # Make sure that the neighborlist does not contain the same pairs twice. # Reoccuring entries may be due to small system size. In this case, the # Hessian matrix will not be symmetric. unique_pairs = set((i, j) for (i, j) in zip(i_n, j_n)) if len(unique_pairs) != len(i_n): raise ValueError("neighborlist contains some pairs more than once") assert (np.all(i_n != j_n)) if divide_by_masses: masses_i = atoms.get_masses().reshape(-1, 1, 1) geom_mean_mass_n = np.sqrt( np.take(masses_i, i_n) * np.take(masses_i, j_n)).reshape( -1, 1, 1) # Calculate the derivatives of the pair energy drep_n = np.zeros_like(abs_dr_n) # first derivative ddrep_n = np.zeros_like(abs_dr_n) # second derivative for atidx1, atnum1 in enumerate(self._db_atomic_numbers): rep1 = self.rep[atidx1] drep1 = self.drep[atidx1] ddrep1 = self.ddrep[atidx1] mask1 = atnums[i_n] == atnum1 if mask1.sum() > 0: for atidx2, atnum2 in enumerate(self._db_atomic_numbers): rep = rep1[atidx2] drep = drep1[atidx2] ddrep = ddrep1[atidx2] mask = np.logical_and(mask1, atnums[j_n] == atnum2) if mask.sum() > 0: r = rep(abs_dr_n[mask]) / abs_dr_n[mask] drep_n[mask] = (drep(abs_dr_n[mask]) - r) / abs_dr_n[mask] ddrep_n[mask] = (ddrep(abs_dr_n[mask]) - 2.0 * drep_n[mask]) / abs_dr_n[mask] # Calculate the total electron density at each atom, and the # derivatives of pairwise contributions f_n = np.zeros_like(abs_dr_n) df_n = np.zeros_like(abs_dr_n) # first derivative ddf_n = np.zeros_like(abs_dr_n) # second derivative for atidx1, atnum1 in enumerate(self._db_atomic_numbers): f1 = self.f[atidx1] df1 = self.df[atidx1] ddf1 = self.ddf[atidx1] mask1 = atnums[j_n] == atnum1 if mask1.sum() > 0: if type(f1) == list: for atidx2, atnum2 in enumerate(self._db_atomic_numbers): f = f1[atidx2] df = df1[atidx2] ddf = ddf1[atidx2] mask = np.logical_and(mask1, atnums[i_n] == atnum2) if mask.sum() > 0: f_n[mask] = f(abs_dr_n[mask]) df_n[mask] = df(abs_dr_n[mask]) ddf_n[mask] = ddf(abs_dr_n[mask]) else: f_n[mask1] = f1(abs_dr_n[mask1]) df_n[mask1] = df1(abs_dr_n[mask1]) ddf_n[mask1] = ddf1(abs_dr_n[mask1]) # Accumulate density contributions density_i = np.bincount(i_n, weights=f_n, minlength=nat) # Calculate the derivatives of the embedding energy demb_i = np.zeros(nat) # first derivative ddemb_i = np.zeros(nat) # second derivative for atidx, atnum in enumerate(self._db_atomic_numbers): F = self.F[atidx] dF = self.dF[atidx] ddF = self.ddF[atidx] mask = atnums == atnum if mask.sum() > 0: demb_i[mask] += dF(density_i[mask]) ddemb_i[mask] += ddF(density_i[mask]) symmetry_check = False # check symmetry of individual terms? #------------------------------------------------------------------------ # Calculate pair contribution to the Hessian matrix #------------------------------------------------------------------------ e_nc = (dr_nc.T / abs_dr_n).T # normalized distance vectors r_i^{\mu\nu} outer_1 = e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3) outer_2 = np.eye(3, dtype=e_nc.dtype) - outer_1 D_ncc = -(ddrep_n * outer_1.T).T D_ncc += -(drep_n / abs_dr_n * outer_2.T).T if divide_by_masses: D = bsr_matrix((D_ncc / geom_mean_mass_n, j_n, first_i), shape=(3 * nat, 3 * nat)) else: D = bsr_matrix((D_ncc, j_n, first_i), shape=(3 * nat, 3 * nat)) Ddiag = np.empty((nat, 3, 3)) for x in range(3): for y in range(3): Ddiag[:, x, y] = -np.bincount( i_n, weights=D_ncc[:, x, y]) # summation if divide_by_masses: Ddiag /= masses_i # put 3x3 blocks on diagonal (Kronecker Delta delta_{\mu\nu}) D += bsr_matrix((Ddiag, np.arange(nat), np.arange(nat + 1)), shape=(3 * nat, 3 * nat)) #------------------------------------------------------------------------ # Calculate contribution of embedding term #------------------------------------------------------------------------ # For each pair in the neighborlist, create arrays which store the # derivatives of the embedding energy of the corresponding atoms. demb_i_n = np.take(demb_i, i_n) demb_j_n = np.take(demb_i, j_n) ddemb_i_n = np.take(ddemb_i, i_n) ddemb_j_n = np.take(ddemb_i, j_n) # Let r be an index into the neighbor list. df_n[r] contains the the # contribution from atom j_n[r] to the derivative of the electron # density of atom i_n[r]. We additionally need the contribution of # i_n[r] to the derivative of j_n[r]. This value is also in df_n, # but at a different position. reverse[r] gives the new index s # where we find this value. The same indexing applies to ddf_n. reverse = find_indices_of_reversed_pairs(i_n, j_n, abs_dr_n) df_i_n = np.take(df_n, reverse) ddf_i_n = np.take(ddf_n, reverse) #we already have ddf_j_n = ddf_n # Term 1: # \delta_{\nu\mu}U_\nu'' # \sum_{\gamma\neq\nu}^{\natoms}g_{\nu\gamma}'\frac{r_{\nu\gamma i}}{r_{\nu\gamma}} # \sum_{\gamma\neq\nu}^{\natoms}g_{\nu\gamma}'\frac{r_{\nu\gamma j}}{r_{\nu\gamma}} # Likely zero in equilibrium because the sum is zero (appears in the force vector) df_n_e_nc_outer_product = (df_n * e_nc.T).T df_n_e_nc_i = np.empty((nat, 3), dtype=df_n.dtype) for x in range(3): df_n_e_nc_i[:, x] = np.bincount(i_n, weights=df_n_e_nc_outer_product[:, x], minlength=nat) term_1_ncc = ((ddemb_i * df_n_e_nc_i.T).T).reshape( -1, 3, 1) * df_n_e_nc_i.reshape(-1, 1, 3) if divide_by_masses: term_1_ncc /= masses_i term_1 = bsr_matrix((term_1_ncc, np.arange(nat), np.arange(nat + 1)), shape=(3 * nat, 3 * nat)) D += term_1 if symmetry_check: print("check term 1", np.linalg.norm(term_1.todense() - term_1.todense().T)) # Term 2: # -u_\nu''g_{\nu\mu}' \frac{r_{\nu\mu j}}{r_{\nu\mu}} \sum_{\gamma\neq\nu}^{\natoms} # g_{\nu\gamma}' \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} # Likely zero in equilibrium because the sum is zero (appears in the force vector) df_n_e_nc_j_n = np.take(df_n_e_nc_i, j_n, axis=0) term_2_ncc = ((ddemb_j_n * df_i_n * e_nc.T).T).reshape( -1, 3, 1) * df_n_e_nc_j_n.reshape(-1, 1, 3) if divide_by_masses: term_2_ncc /= geom_mean_mass_n term_2 = bsr_matrix((term_2_ncc, j_n, first_i), shape=(3 * nat, 3 * nat)) D += term_2 if symmetry_check: print("check term 2", np.linalg.norm(term_2.todense() - term_2.todense().T)) # Term 3: # +u_\mu''g_{\mu\nu}' \frac{r_{\nu\mu i}}{r_{\nu\mu}} \sum_{\gamma\neq\mu}^{\natoms} # g_{\mu\gamma}' \frac{r_{\mu\gamma j}}{r_{\mu\gamma}} # Likely zero in equilibrium because the sum is zero (appears in the force vector) df_n_e_nc_i_n = np.take(df_n_e_nc_i, i_n, axis=0) term_3_ncc = -((ddemb_i_n * df_n * df_n_e_nc_i_n.T).T).reshape( -1, 3, 1) * e_nc.reshape(-1, 1, 3) if divide_by_masses: term_3_ncc /= geom_mean_mass_n term_3 = bsr_matrix((term_3_ncc, j_n, first_i), shape=(3 * nat, 3 * nat)) D += term_3 if symmetry_check: print("check term 3", np.linalg.norm(term_3.todense() - term_3.todense().T)) # Term 4: # -\left(u_\mu'g_{\mu\nu}'' + u_\nu'g_{\nu\mu}''\right) # \left( # \frac{r_{\nu\mu i}}{r_{\nu\mu}} # \frac{r_{\nu\mu j}}{r_{\nu\mu}} # \right) tmp_1 = -((demb_j_n * ddf_i_n + demb_i_n * ddf_n) * outer_1.T).T # We don't immediately add term 4 to the matrix, because it would have # to be normalized by the masses if divide_by_masses is true. However, # for construction of term 5, we need term 4 without normalization # Term 5: # \delta_{\nu\mu} \sum_{\gamma\neq\nu}^{\natoms} #\left(U_\gamma'g_{\gamma\nu}'' + U_\nu'g_{\nu\gamma}''\right) #\left( #\frac{r_{\nu\gamma i}}{r_{\nu\gamma}} #\frac{r_{\nu\gamma j}}{r_{\nu\gamma}} #\right) tmp_1_summed = np.empty((nat, 3, 3), dtype=tmp_1.dtype) for x in range(3): for y in range(3): tmp_1_summed[:, x, y] = -np.bincount(i_n, weights=tmp_1[:, x, y]) if divide_by_masses: tmp_1_summed /= masses_i term_5 = bsr_matrix((tmp_1_summed, np.arange(nat), np.arange(nat + 1)), shape=(3 * nat, 3 * nat)) D += term_5 if symmetry_check: print("check term 5", np.linalg.norm(term_5.todense() - term_5.todense().T)) if divide_by_masses: tmp_1 /= geom_mean_mass_n term_4 = bsr_matrix((tmp_1, j_n, first_i), shape=(3 * nat, 3 * nat)) D += term_4 if symmetry_check: print("check term 4", np.linalg.norm(term_4.todense() - term_4.todense().T)) # Term 6: # -\left(U_\mu'g_{\mu\nu}' + U_\nu'g_{\nu\mu}'\right) \frac{1}{r_{\nu\mu}} #\left( #\delta_{ij}- #\frac{r_{\nu\mu i}}{r_{\nu\mu}} #\frac{r_{\nu\mu j}}{r_{\nu\mu}} #\right) # Like term 4, which was needed to construct term 5, we don't add # term 6 immediately, because it is needed for construction of term 7 tmp_2 = -( (demb_j_n * df_i_n + demb_i_n * df_n) / abs_dr_n * outer_2.T).T # Term 7: # \delta_{\nu\mu} \sum_{\gamma\neq\nu}^{\natoms} #\left(U_\gamma'g_{\gamma\nu}' + U_\nu'g_{\nu\gamma}'\right) \frac{1}{r_{\nu\gamma}} #\left(\delta_{ij}- #\frac{r_{\nu\gamma i}}{r_{\nu\gamma}} #\frac{r_{\nu\gamma j}}{r_{\nu\gamma}} #\right) tmp_2_summed = np.empty((nat, 3, 3), dtype=tmp_2.dtype) for x in range(3): for y in range(3): tmp_2_summed[:, x, y] = -np.bincount(i_n, weights=tmp_2[:, x, y]) if divide_by_masses: tmp_2_summed /= masses_i term_7 = bsr_matrix((tmp_2_summed, np.arange(nat), np.arange(nat + 1)), shape=(3 * nat, 3 * nat)) D += term_7 if symmetry_check: print("check term 7", np.linalg.norm(term_7.todense() - term_7.todense().T)) if divide_by_masses: tmp_2 /= geom_mean_mass_n term_6 = bsr_matrix((tmp_2, j_n, first_i), shape=(3 * nat, 3 * nat)) D += term_6 if symmetry_check: print("check term 6", np.linalg.norm(term_6.todense() - term_6.todense().T)) #Term 8: # \sum_{\substack{\gamma\neq\nu \\ \gamma \neq \mu}}^{\natoms} #U_\gamma'' g_{\gamma\nu}'g_{\gamma\mu}' #\frac{r_{\gamma\nu i}}{r_{\gamma\nu}} #\frac{r_{\gamma\mu j}}{r_{\gamma\mu}} #----------------------------------------------------------------------- # This term requires knowledge of common neighbors of pairs of atoms. # Construction of a common neighbor list in Python is likely a # performance bottleneck; it should be implemented in C++ instead. #----------------------------------------------------------------------- # For each (i1, j2)-pair in the neighbor list, find all other # pairs (i2, j) which share the same j. This includes (i1, j) # itself. In this way, create a list with n blocks of rows, where # n is the length of the neighbor list. All rows in a block have # the same j. Each row corresponds to one triplet i1, j, i2. # The number of rows in the block is equal to the total number # of neighbors of j. This is the common neighbor list (cnl). # Besides the block number and i1, j, i2, the cnl stores the # index into the neighbor list where the pair i2-j can be found. j_order = np.argsort(j_n) i_n_2 = i_n[j_order] j_n_2 = j_n[j_order] first_j = first_neighbours(nat, j_n_2) num_rows_per_j = first_j[j_n + 1] - first_j[j_n] total_num_rows = np.sum(num_rows_per_j) # The common neighbor information could be stored as # a 2D array. However, multiple 1D arrays are likely # better for performance (fewer cache misses later). cnl_block_number = np.empty(total_num_rows, dtype=i_n.dtype) cnl_j1 = np.empty(total_num_rows, dtype=i_n.dtype) cnl_j_order = np.empty(total_num_rows, dtype=i_n.dtype) cnl_i1_i2 = np.empty((total_num_rows, 2), dtype=i_n.dtype) block_start = np.r_[0, np.cumsum(num_rows_per_j)] slice_for_j1 = { j1: slice(first_j[j1], first_j[j1 + 1]) for j1 in np.arange(nat) } for block_number, (i1, j1) in enumerate(zip(i_n, j_n)): slice1 = slice(block_start[block_number], block_start[block_number + 1]) slice2 = slice_for_j1[j1] cnl_block_number[slice1] = block_number cnl_j1[slice1] = j1 cnl_j_order[slice1] = j_order[slice2] cnl_i1_i2[slice1, 0] = i1 cnl_i1_i2[slice1, 1] = i_n_2[slice2] # Determine bins for accumulation by bincount unique_pairs_i1_i2, bincount_bins = np.unique(cnl_i1_i2, axis=0, return_inverse=True) e_nu_cnl = np.take(e_nc, cnl_block_number, axis=0) e_mu_cnl = np.take(e_nc, cnl_j_order, axis=0) ddemb_cnl = np.take(ddemb_i, cnl_j1) df_nu_cnl = np.take(df_i_n, cnl_block_number) df_mu_cnl = np.take(df_i_n, cnl_j_order) tmp_3 = ((ddemb_cnl * df_nu_cnl * df_mu_cnl * e_nu_cnl.T).T).reshape( -1, 3, 1) * e_mu_cnl.reshape(-1, 1, 3) tmp_3_summed = np.empty((unique_pairs_i1_i2.shape[0], 3, 3), dtype=e_nc.dtype) for x in range(3): for y in range(3): tmp_3_summed[:, x, y] = np.bincount( bincount_bins, weights=tmp_3[:, x, y], minlength=unique_pairs_i1_i2.shape[0]) if divide_by_masses: geom_mean_mass_i1_i2 = np.sqrt( np.take(masses_i, unique_pairs_i1_i2[:, 0]) * np.take(masses_i, unique_pairs_i1_i2[:, 1])) tmp_3_summed /= geom_mean_mass_i1_i2[:, np.newaxis, np.newaxis] index_ptr = first_neighbours(nat, unique_pairs_i1_i2[:, 0]) term_8 = bsr_matrix( (tmp_3_summed, unique_pairs_i1_i2[:, 1], index_ptr), shape=(3 * nat, 3 * nat)) if symmetry_check: print("check term 8", np.linalg.norm(term_8.todense() - term_8.todense().T)) D += term_8 return D
def _calculate_hessian_embedding_term_8(self, nat, i_n, j_n, e_nc, ddemb_i, df_i_n, divide_by_masses=False, masses_i=None, geom_mean_mass_n=None, symmetry_check=False): """Calculate term 8 in the embedding part of the Hessian matrix. .. math:: T_{\nu\mu}^{(8)} = \sum_{\substack{\gamma\neq\nu \\ \gamma \neq \mu}}^{N} U_\gamma'' g_{\gamma\nu}'g_{\gamma\mu}' \frac{r_{\gamma\nu i}}{r_{\gamma\nu}} \frac{r_{\gamma\mu j}}{r_{\gamma\mu}} This term requires knowledge of common neighbors of pairs of atoms. Parameters ---------- nat : int Number of atoms i_n, j_n : array_like Neighbor pairs first_i : array_like Indices in :code:`i_n` where contiguous blocks with the same value start ddemb_i : array_like Second derivative of the embedding energy e_nc : array_like Normalized distance vectors between neighbors df_i_n : array_like Derivative of the electron density of atom :code:`j` with respect to the distance to atom :code:`i` divide_by_masses : bool Divide term by geometric mean of mass of pairs of atoms to obtain the contribution to the dynamical matrix masses_i : array_like masses of atoms :code:`i` geom_mean_mass_n : array_like geometric mean of masses of pairs of atoms symmetry_check : bool Check if the terms are symmetric Returns ------- D : scipy.sparse.bsr_matrix """ cnl_i1_i2, cnl_j1, nl_index_i1_j1, nl_index_i2_j1 = find_common_neighbours( i_n, j_n, nat) unique_pairs_i1_i2, bincount_bins = np.unique(cnl_i1_i2, axis=0, return_inverse=True) cnl_i1_i2 = None tmp_3 = np.take(df_i_n, nl_index_i1_j1) * np.take( ddemb_i, cnl_j1) * np.take(df_i_n, nl_index_i2_j1) cnl_j1 = None tmp_3_summed = np.empty((unique_pairs_i1_i2.shape[0], 3, 3), dtype=e_nc.dtype) for x, y in np.ndindex(3, 3): weights = (tmp_3 * np.take(e_nc[:, x], nl_index_i1_j1) * np.take(e_nc[:, y], nl_index_i2_j1)) tmp_3_summed[:, x, y] = np.bincount( bincount_bins, weights=weights, minlength=unique_pairs_i1_i2.shape[0]) nl_index_i1_j1 = None nl_index_i2_j1 = None weights = None tmp_3 = None bincount_bins = None if divide_by_masses: geom_mean_mass_i1_i2 = np.sqrt( np.take(masses_i, unique_pairs_i1_i2[:, 0]) * np.take(masses_i, unique_pairs_i1_i2[:, 1])) tmp_3_summed /= geom_mean_mass_i1_i2[:, np.newaxis, np.newaxis] index_ptr = first_neighbours(nat, unique_pairs_i1_i2[:, 0]) term_8 = bsr_matrix( (tmp_3_summed, unique_pairs_i1_i2[:, 1], index_ptr), shape=(3 * nat, 3 * nat)) if symmetry_check: print("check term 8", np.linalg.norm(term_8.todense() - term_8.todense().T)) return term_8
def calculate_hessian_matrix(self, atoms, divide_by_masses=False): r"""Compute the Hessian matrix The Hessian matrix is the matrix of second derivatives of the potential energy :math:`\mathcal{V}_\mathrm{int}` with respect to coordinates, i.e.\ .. math:: \frac{\partial^2 \mathcal{V}_\mathrm{int}} {\partial r_{\nu{}i}\partial r_{\mu{}j}}, where the indices :math:`\mu` and :math:`\nu` refer to atoms and the indices :math:`i` and :math:`j` refer to the components of the position vector :math:`r_\nu` along the three spatial directions. The Hessian matrix has contributions from the pair potential and the embedding energy, .. math:: \frac{\partial^2 \mathcal{V}_\mathrm{int}}{\partial r_{\nu{}i}\partial r_{\mu{}j}} = \frac{\partial^2 \mathcal{V}_\mathrm{pair}}{ \partial r_{\nu i} \partial r_{\mu j}} + \frac{\partial^2 \mathcal{V}_\mathrm{embed}}{\partial r_{\nu i} \partial r_{\mu j}}. The contribution from the pair potential is .. math:: \frac{\partial^2 \mathcal{V}_\mathrm{pair}}{ \partial r_{\nu i} \partial r_{\mu j}} &= -\phi_{\nu\mu}'' \left( \frac{r_{\nu\mu i}}{r_{\nu\mu}} \frac{r_{\nu\mu j}}{r_{\nu\mu}} \right) -\frac{\phi_{\nu\mu}'}{r_{\nu\mu}}\left( \delta_{ij}- \frac{r_{\nu\mu i}}{r_{\nu\mu}} \frac{r_{\nu\mu j}}{r_{\nu\mu}} \right) \\ &+\delta_{\nu\mu}\sum_{\gamma\neq\nu}^{N} \phi_{\nu\gamma}'' \left( \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \right) +\delta_{\nu\mu}\sum_{\gamma\neq\nu}^{N}\frac{\phi_{\nu\gamma}'}{r_{\nu\gamma}}\left( \delta_{ij}- \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \right). The contribution of the embedding energy to the Hessian matrix is a sum of eight terms, .. math:: \frac{\mathcal{V}_\mathrm{embed}}{\partial r_{\mu j} \partial r_{\nu i}} &= T_1 + T_2 + T_3 + T_4 + T_5 + T_6 + T_7 + T_8 \\ T_1 &= \delta_{\nu\mu}U_\nu'' \sum_{\gamma\neq\nu}^{N}g_{\nu\gamma}'\frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \sum_{\gamma\neq\nu}^{N}g_{\nu\gamma}'\frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \\ T_2 &= -u_\nu''g_{\nu\mu}' \frac{r_{\nu\mu j}}{r_{\nu\mu}} \sum_{\gamma\neq\nu}^{N} G_{\nu\gamma}' \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \\ T_3 &= +u_\mu''g_{\mu\nu}' \frac{r_{\nu\mu i}}{r_{\nu\mu}} \sum_{\gamma\neq\mu}^{N} G_{\mu\gamma}' \frac{r_{\mu\gamma j}}{r_{\mu\gamma}} \\ T_4 &= -\left(u_\mu'g_{\mu\nu}'' + u_\nu'g_{\nu\mu}''\right) \left( \frac{r_{\nu\mu i}}{r_{\nu\mu}} \frac{r_{\nu\mu j}}{r_{\nu\mu}} \right)\\ T_5 &= \delta_{\nu\mu} \sum_{\gamma\neq\nu}^{N} \left(U_\gamma'g_{\gamma\nu}'' + U_\nu'g_{\nu\gamma}''\right) \left( \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \right) \\ T_6 &= -\left(U_\mu'g_{\mu\nu}' + U_\nu'g_{\nu\mu}'\right) \frac{1}{r_{\nu\mu}} \left( \delta_{ij}- \frac{r_{\nu\mu i}}{r_{\nu\mu}} \frac{r_{\nu\mu j}}{r_{\nu\mu}} \right) \\ T_7 &= \delta_{\nu\mu} \sum_{\gamma\neq\nu}^{N} \left(U_\gamma'g_{\gamma\nu}' + U_\nu'g_{\nu\gamma}'\right) \frac{1}{r_{\nu\gamma}} \left(\delta_{ij}- \frac{r_{\nu\gamma i}}{r_{\nu\gamma}} \frac{r_{\nu\gamma j}}{r_{\nu\gamma}} \right) \\ T_8 &= \sum_{\substack{\gamma\neq\nu \\ \gamma \neq \mu}}^{N} U_\gamma'' g_{\gamma\nu}'g_{\gamma\mu}' \frac{r_{\gamma\nu i}}{r_{\gamma\nu}} \frac{r_{\gamma\mu j}}{r_{\gamma\mu}} Parameters ---------- atoms : ase.Atoms divide_by_masses : bool Divide block :math:`\nu\mu` by :math:`m_\nu{}m_\mu{}` to obtain the dynamical matrix Returns ------- D : numpy.matrix Block Sparse Row matrix with the nonzero blocks Notes ----- Notation: * :math:`N` Number of atoms * :math:`\mathcal{V}_\mathrm{int}` Total potential energy * :math:`\mathcal{V}_\mathrm{pair}` Pair potential * :math:`\mathcal{V}_\mathrm{embed}` Embedding energy * :math:`r_{\nu{}i}` Component :math:`i` of the position vector of atom :math:`\nu` * :math:`r_{\nu\mu{}i} = r_{\mu{}i}-r_{\nu{}i}` * :math:`r_{\nu\mu{}}` Norm of :math:`r_{\nu\mu{}i}`, i.e.\ :math:`\left(r_{\nu\mu{}1}^2+r_{\nu\mu{}2}^2+r_{\nu\mu{}3}^2\right)^{1/2}` * :math:`\phi_{\nu\mu}(r_{\nu\mu{}})` Pair potential energy of atoms :math:`\nu` and :math:`\mu` * :math:`\rho_{\nu}` Total electron density of atom :math:`\nu` * :math:`U_\nu(\rho_nu)` Embedding energy of atom :math:`\nu` * :math:`g_{\delta}\left(r_{\gamma\delta}\right) \equiv g_{\gamma\delta}` Contribution from atom :math:`\delta` to :math:`\rho_\gamma` * :math:`m_\nu` mass of atom :math:`\nu` """ nat = len(atoms) atnums = atoms.numbers atnums_in_system = set(atnums) for atnum in atnums_in_system: if atnum not in atnums: raise RuntimeError('Element with atomic number {} found, but ' 'this atomic number has no EAM ' 'parametrization'.format(atnum)) # i_n: index of the central atom # j_n: index of the neighbor atom # dr_nc: distance vector between the two # abs_dr_n: norm of distance vector # Variable name ending with _n indicate arrays that contain # one element for each pair in the neighbor list. Names ending # with _i indicate arrays containing one element for each atom. i_n, j_n, dr_nc, abs_dr_n = neighbour_list('ijDd', atoms, self._db_cutoff) # Calculate derivatives of the pair energy drep_n = np.zeros_like(abs_dr_n) # first derivative ddrep_n = np.zeros_like(abs_dr_n) # second derivative for atidx1, atnum1 in enumerate(self._db_atomic_numbers): rep1 = self.rep[atidx1] drep1 = self.drep[atidx1] ddrep1 = self.ddrep[atidx1] mask1 = atnums[i_n] == atnum1 if mask1.sum() > 0: for atidx2, atnum2 in enumerate(self._db_atomic_numbers): rep = rep1[atidx2] drep = drep1[atidx2] ddrep = ddrep1[atidx2] mask = np.logical_and(mask1, atnums[j_n] == atnum2) if mask.sum() > 0: r = rep(abs_dr_n[mask]) / abs_dr_n[mask] drep_n[mask] = (drep(abs_dr_n[mask]) - r) / abs_dr_n[mask] ddrep_n[mask] = (ddrep(abs_dr_n[mask]) - 2.0 * drep_n[mask]) / abs_dr_n[mask] # Calculate electron density and its derivatives f_n = np.zeros_like(abs_dr_n) df_n = np.zeros_like(abs_dr_n) # first derivative ddf_n = np.zeros_like(abs_dr_n) # second derivative for atidx1, atnum1 in enumerate(self._db_atomic_numbers): f1 = self.f[atidx1] df1 = self.df[atidx1] ddf1 = self.ddf[atidx1] mask1 = atnums[j_n] == atnum1 if mask1.sum() > 0: if type(f1) == list: for atidx2, atnum2 in enumerate(self._db_atomic_numbers): f = f1[atidx2] df = df1[atidx2] ddf = ddf1[atidx2] mask = np.logical_and(mask1, atnums[i_n] == atnum2) if mask.sum() > 0: f_n[mask] = f(abs_dr_n[mask]) df_n[mask] = df(abs_dr_n[mask]) ddf_n[mask] = ddf(abs_dr_n[mask]) else: f_n[mask1] = f1(abs_dr_n[mask1]) df_n[mask1] = df1(abs_dr_n[mask1]) ddf_n[mask1] = ddf1(abs_dr_n[mask1]) # Accumulate density contributions density_i = np.bincount(i_n, weights=f_n, minlength=nat) # Calculate the derivatives of the embedding energy demb_i = np.zeros(nat) # first derivative ddemb_i = np.zeros(nat) # second derivative for atidx, atnum in enumerate(self._db_atomic_numbers): F = self.F[atidx] dF = self.dF[atidx] ddF = self.ddF[atidx] mask = atnums == atnum if mask.sum() > 0: demb_i[mask] += dF(density_i[mask]) ddemb_i[mask] += ddF(density_i[mask]) # There are two ways to divide the Hessian by atomic masses, either # during or after construction. The former is preferable with regard # to memory consumption. If we would divide by masses afterwards, # we would have to create a sparse matrix with the same size as the # Hessian matrix, i.e. we would momentarily need twice the given memory. if divide_by_masses: masses_i = atoms.get_masses().reshape(-1, 1, 1) geom_mean_mass_n = np.sqrt( np.take(masses_i, i_n) * np.take(masses_i, j_n)).reshape( -1, 1, 1) else: masses_i = None geom_mean_mass_n = None #------------------------------------------------------------------------ # Calculate pair contribution to the Hessian matrix #------------------------------------------------------------------------ first_i = first_neighbours(nat, i_n) e_nc = (dr_nc.T / abs_dr_n).T # normalized distance vectors r_i^{\mu\nu} outer_e_ncc = e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3) eye_minus_outer_e_ncc = np.eye(3, dtype=e_nc.dtype) - outer_e_ncc D = self._calculate_hessian_pair_term(nat, i_n, j_n, abs_dr_n, first_i, drep_n, ddrep_n, outer_e_ncc, eye_minus_outer_e_ncc, divide_by_masses, geom_mean_mass_n, masses_i) drep_n = None ddrep_n = None #------------------------------------------------------------------------ # Calculate contribution of embedding term #------------------------------------------------------------------------ # For each pair in the neighborlist, create arrays which store the # derivatives of the embedding energy of the corresponding atoms. demb_i_n = np.take(demb_i, i_n) demb_j_n = np.take(demb_i, j_n) # Let r be an index into the neighbor list. df_n[r] contains the the # contribution from atom j_n[r] to the derivative of the electron # density of atom i_n[r]. We additionally need the contribution of # i_n[r] to the derivative of j_n[r]. This value is also in df_n, # but at a different position. reverse[r] gives the new index s # where we find this value. The same indexing applies to ddf_n. reverse = find_indices_of_reversed_pairs(i_n, j_n, abs_dr_n) df_i_n = np.take(df_n, reverse) ddf_i_n = np.take(ddf_n, reverse) #we already have ddf_j_n = ddf_n reverse = None df_n_e_nc_outer_product = (df_n * e_nc.T).T df_e_ni = np.empty((nat, 3), dtype=df_n.dtype) for x in range(3): df_e_ni[:, x] = np.bincount(i_n, weights=df_n_e_nc_outer_product[:, x], minlength=nat) df_n_e_nc_outer_product = None D += self._calculate_hessian_embedding_term_1(nat, ddemb_i, df_e_ni, divide_by_masses, masses_i) D += self._calculate_hessian_embedding_term_2(nat, j_n, first_i, ddemb_i, df_i_n, e_nc, df_e_ni, divide_by_masses, geom_mean_mass_n) D += self._calculate_hessian_embedding_term_3(nat, i_n, j_n, first_i, ddemb_i, df_n, e_nc, df_e_ni, divide_by_masses, geom_mean_mass_n) df_e_ni = None D += self._calculate_hessian_embedding_terms_4_and_5( nat, first_i, i_n, j_n, outer_e_ncc, demb_i_n, demb_j_n, ddf_i_n, ddf_n, divide_by_masses, masses_i, geom_mean_mass_n) outer_e_ncc = None ddf_i_n = None ddf_n = None D += self._calculate_hessian_embedding_terms_6_and_7( nat, i_n, j_n, first_i, abs_dr_n, eye_minus_outer_e_ncc, demb_i_n, demb_j_n, df_n, df_i_n, divide_by_masses, masses_i, geom_mean_mass_n) eye_minus_outer_e_ncc = None df_n = None demb_i_n = None demb_j_n = None abs_dr_n = None D += self._calculate_hessian_embedding_term_8(nat, i_n, j_n, e_nc, ddemb_i, df_i_n, divide_by_masses, masses_i, geom_mean_mass_n) return D
def calculate_hessian_matrix(self, atoms, H_format="dense", limits=None): """ Calculate the Hessian matrix for a pair potential. For an atomic configuration with N atoms in d dimensions the hessian matrix is a symmetric, hermitian matrix with a shape of (d*N,d*N). The matrix is in general a sparse matrix, which consists of dense blocks of shape (d,d), which are the mixed second derivatives. The result of the derivation for a pair potential can be found in: L. Pastewka et. al. "Seamless elastic boundaries for atomistic calculations", Phys. Ev. B 86, 075459 (2012). Parameters ---------- atoms: ase.Atoms Atomic configuration in a local or global minima. H_format: "dense" or "sparse" Output format of the hessian matrix. The format "sparse" is only possible if matscipy was build with scipy. limits: list [atomID_low, atomID_up] Calculate the Hessian matrix only for the given atom IDs. If limits=[5,10] the Hessian matrix is computed for atom IDs 5,6,7,8,9 only. The Hessian matrix will have the full shape dim(3*N,3*N) where N is the number of atoms. This ensures correct indexing of the data. Restrictions ---------- This method is currently only implemented for three dimensional systems """ if H_format == "sparse": try: from scipy.sparse import bsr_matrix, vstack, hstack except ImportError: raise ImportError( "Import error: Can not output the hessian matrix since scipy.sparse could not be loaded!") f = self.f dict = self.dict df = self.df df2 = self.df2 nat = len(atoms) atnums = atoms.numbers i_n, j_n, dr_nc, abs_dr_n = neighbour_list('ijDd', atoms, dict) first_i = first_neighbours(nat, i_n) e_n = np.zeros_like(abs_dr_n) de_n = np.zeros_like(abs_dr_n) dde_n = np.zeros_like(abs_dr_n) for params, pair in enumerate(dict): if pair[0] == pair[1]: mask1 = atnums[i_n] == pair[0] mask2 = atnums[j_n] == pair[0] mask = np.logical_and(mask1, mask2) e_n[mask] = f[pair](abs_dr_n[mask]) de_n[mask] = df[pair](abs_dr_n[mask]) dde_n[mask] = df2[pair](abs_dr_n[mask]) if pair[0] != pair[1]: mask1 = np.logical_and( atnums[i_n] == pair[0], atnums[j_n] == pair[1]) mask2 = np.logical_and( atnums[i_n] == pair[1], atnums[j_n] == pair[0]) mask = np.logical_or(mask1, mask2) e_n[mask] = f[pair](abs_dr_n[mask]) de_n[mask] = df[pair](abs_dr_n[mask]) dde_n[mask] = df2[pair](abs_dr_n[mask]) if limits != None: if limits[1] < limits[0]: raise ValueError( "Value error: The upper atom id cannot be smaller than the lower atom id.") else: mask = np.logical_and(i_n >= limits[0], i_n < limits[1]) i_n = i_n[mask] i_n1 = i_n - i_n[0] j_n = j_n[mask] dr_nc = dr_nc[mask] abs_dr_n = abs_dr_n[mask] e_n = e_n[mask] de_n = de_n[mask] dde_n = dde_n[mask] nat1 = limits[1] - limits[0] first_i = [0] * (nat1 + 1) j = 1 for k in range(1, len(i_n)): if i_n[k] != i_n[k-1]: first_i[j] = k j = j+1 first_i[-1] = len(i_n) if H_format == "sparse": # Off-diagonal elements of the Hessian matrix e_nc = (dr_nc.T / abs_dr_n).T H_ncc = -(dde_n * (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3)).T).T H_ncc += -(de_n / abs_dr_n * (np.eye(3, dtype=e_nc.dtype) - (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3))).T).T H_nat1nat = bsr_matrix( (H_ncc, j_n, first_i), shape=(3*nat1, 3*nat)) # Stack matrices in order to obtain full shape (3*nat, 3*nat) H = vstack([bsr_matrix((limits[0]*3, 3*nat)), H_nat1nat, bsr_matrix((3*nat - limits[1]*3, 3*nat))]) # Diagonal elements of the Hessian matrix Hdiag_icc = np.empty((nat1, 3, 3)) for x in range(3): for y in range(3): Hdiag_icc[:, x, y] = - \ np.bincount(i_n1, weights=H_ncc[:, x, y]) Hdiag_nat1nat = bsr_matrix((Hdiag_icc, np.arange(limits[0], limits[1]), np.arange(nat1+1)), shape=(3*nat1, 3*nat)) # Compute full Hessian matrix H += vstack([bsr_matrix((limits[0]*3, 3*nat)), Hdiag_nat1nat, bsr_matrix((3*nat - limits[1]*3, 3*nat))]) return H elif H_format == "dense": # Off-diagonal elements of the Hessian matrix e_nc = (dr_nc.T / abs_dr_n).T H_ncc = -(dde_n * (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3)).T).T H_ncc += -(de_n/abs_dr_n * (np.eye(3, dtype=e_nc.dtype) - (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3))).T).T H = np.zeros((3*nat, 3*nat)) for atom in range(len(i_n)): H[3*i_n[atom]:3*i_n[atom]+3, 3*j_n[atom]:3*j_n[atom]+3] += H_ncc[atom] # Diagonal elements of the Hessian matrix Hdiag_icc = np.empty((nat1, 3, 3)) for x in range(3): for y in range(3): Hdiag_icc[:, x, y] = - \ np.bincount(i_n1, weights=H_ncc[:, x, y]) Hdiag_ncc = np.zeros((3*nat, 3*nat)) for atom in range(nat1): Hdiag_ncc[3*(atom+limits[0]):3*(atom+limits[0])+3, 3*(atom+limits[0]):3*(atom+limits[0])+3] += Hdiag_icc[atom] # Compute full Hessian matrix H += Hdiag_ncc return H # Sparse BSR-matrix elif H_format == "sparse": e_nc = (dr_nc.T/abs_dr_n).T H_ncc = -(dde_n * (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3)).T).T H_ncc += -(de_n/abs_dr_n * (np.eye(3, dtype=e_nc.dtype) - (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3))).T).T H = bsr_matrix((H_ncc, j_n, first_i), shape=(3*nat, 3*nat)) Hdiag_icc = np.empty((nat, 3, 3)) for x in range(3): for y in range(3): Hdiag_icc[:, x, y] = - \ np.bincount(i_n, weights=H_ncc[:, x, y]) H += bsr_matrix((Hdiag_icc, np.arange(nat), np.arange(nat+1)), shape=(3*nat, 3*nat)) return H # Dense matrix format elif H_format == "dense": e_nc = (dr_nc.T/abs_dr_n).T H_ncc = -(dde_n * (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3)).T).T H_ncc += -(de_n/abs_dr_n * (np.eye(3, dtype=e_nc.dtype) - (e_nc.reshape(-1, 3, 1) * e_nc.reshape(-1, 1, 3))).T).T H = np.zeros((3*nat, 3*nat)) for atom in range(len(i_n)): H[3*i_n[atom]:3*i_n[atom]+3, 3*j_n[atom]:3*j_n[atom]+3] += H_ncc[atom] Hdiag_icc = np.empty((nat, 3, 3)) for x in range(3): for y in range(3): Hdiag_icc[:, x, y] = - \ np.bincount(i_n, weights=H_ncc[:, x, y]) Hdiag_ncc = np.zeros((3*nat, 3*nat)) for atom in range(nat): Hdiag_ncc[3*atom:3*atom+3, 3*atom:3*atom+3] += Hdiag_icc[atom] H += Hdiag_ncc return H
def hydrogenate(a, cutoff, bond_length, b=None, mask=[True, True, True], exclude=None, vacuum=None): """ Hydrogenate a slab of material at its periodic boundary conditions. Boundary conditions are turned into nonperiodic. Parameters ---------- a : ase.Atoms Atomic configuration. cutoff : float Cutoff for neighbor counting. bond_length : float X-H bond length for hydrogenation. b : ase.Atoms, optional If present, this is the configuration to hydrogenate. Number of atoms must be identical to a object. All bonds present in a but not present in b will be hydrogenated in b. mask : list of bool Cartesian directions which to hydrogenate, only if b argument is not given. exclude : array_like Boolean array masking atoms to be excluded from hydrogenation. vacuum : float, optional Add this much vacuum after hydrogenation. Returns ------- a : ase.Atoms Atomic configuration of the hydrogenated slab. """ if b is None: b = a.copy() b.set_pbc(np.logical_not(mask)) if exclude is None: exclude = np.zeros(len(a), dtype=bool) i_a, j_a, D_a, d_a = neighbour_list('ijDd', a, cutoff) i_b, j_b = neighbour_list('ij', b, cutoff) firstneigh_a = first_neighbours(len(a), i_a) firstneigh_b = first_neighbours(len(b), i_b) coord_a = np.bincount(i_a, minlength=len(a)) coord_b = np.bincount(i_b, minlength=len(b)) hydrogens = [] # Surface atoms have coord_a != coord_b. Those need hydrogenation for k in np.arange(len(a))[np.logical_and(coord_a!=coord_b, np.logical_not(exclude))]: l1_a = firstneigh_a[k] l2_a = firstneigh_a[k+1] l1_b = firstneigh_b[k] l2_b = firstneigh_b[k+1] n_H = 0 for l_a in range(l1_a, l2_a): assert i_a[l_a] == k bond_exists = False for l_b in range(l1_b, l2_b): assert i_b[l_b] == k if j_a[l_a] == j_b[l_b]: bond_exists = True if not bond_exists: # Bond existed before cut hydrogens += [b[k].position+bond_length*D_a[l_a]/d_a[l_a]] n_H += 1 assert n_H == coord_a[k]-coord_b[k] if hydrogens == []: raise RuntimeError('No Hydrogen created.') b += ase.Atoms(['H']*len(hydrogens), hydrogens) if vacuum is not None: axis=[] for i in range(3): if mask[i]: axis += [i] b.center(vacuum, axis=axis) return b
def calculate_hessian_matrix(self, atoms, divide_by_masses=False): """ Calculate the Hessian matrix for a bond order potential. For an atomic configuration with N atoms in d dimensions the hessian matrix is a symmetric, hermitian matrix with a shape of (d*N,d*N). The matrix is in general a sparse matrix, which consists of dense blocks of shape (d,d), which are the mixed second derivatives. Parameters ---------- atoms: ase.Atoms Atomic configuration in a local or global minima. divide_by_masses: bool if true return the dynamic matrix else hessian matrix Returns ------- bsr_matrix either hessian or dynamic matrix Restrictions ---------- This method is currently only implemented for three dimensional systems """ # construct neighbor list i_p, j_p, r_p, r_pc = neighbour_list('ijdD', atoms=atoms, cutoff=2 * self.cutoff) mask_p = r_p > self.cutoff nb_atoms = len(self.atoms) nb_pairs = len(i_p) # reverse pairs tr_p = find_indices_of_reversed_pairs(i_p, j_p, r_p) # normal vectors n_pc = (r_pc.T / r_p).T nx_p, ny_p, nz_p = n_pc.T # construct triplet list first_i = first_neighbours(nb_atoms, i_p) ij_t, ik_t, jk_t = triplet_list(first_i, r_p, self.cutoff, i_p, j_p) first_ij = first_neighbours(len(i_p), ij_t) nb_triplets = len(ij_t) # basic triplet and pair terms G_t = self.G(r_pc[ij_t], r_pc[ik_t]) xi_p = np.bincount(ij_t, weights=G_t, minlength=nb_pairs) F_p = self.F(r_p, xi_p) # Hessian term #4 d1F_p = self.d1F(r_p, xi_p) d1F_p[ mask_p] = 0.0 # we need to explicitly exclude everything with r > cutoff H_temp_pcc = (d1F_p * (np.eye(3) - (n_pc.reshape(-1, 3, 1) * n_pc.reshape(-1, 1, 3))).T / r_p).T H_pcc = -H_temp_pcc # Hessian term #1 d11F_p = self.d11F(r_p, xi_p) d11F_p[mask_p] = 0.0 H_temp_pcc = (d11F_p * (n_pc.reshape(-1, 3, 1) * n_pc.reshape(-1, 1, 3)).T).T H_pcc -= H_temp_pcc # Hessian term #2 d12F_p = self.d12F(r_p, xi_p) d12F_p[mask_p] = 0.0 d2G_tc = self.d2G(r_pc[ij_t], r_pc[ik_t]) H_temp_t = ( d12F_p[ij_t] * (d2G_tc.reshape(-1, 3, 1) * n_pc[ij_t].reshape(-1, 1, 3)).T).T H_temp_pcc = np.empty_like(H_temp_pcc) for x in range(3): for y in range(3): H_temp_pcc[:, x, y] = np.bincount( tr_p[jk_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount( ij_t, weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount( tr_p[ik_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) H_pcc += H_temp_pcc d1G_tc = self.d1G(r_pc[ij_t], r_pc[ik_t]) H_temp_t = ( d12F_p[ij_t] * (d1G_tc.reshape(-1, 3, 1) * n_pc[ij_t].reshape(-1, 1, 3)).T).T for x in range(3): for y in range(3): H_temp_pcc[:, x, y] = -np.bincount( ij_t, weights=H_temp_t[:, x, y], minlength=nb_pairs ) - np.bincount( tr_p[ij_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) H_pcc += H_temp_pcc # Hessian term #5 d2F_p = self.d2F(r_p, xi_p) d2F_p[mask_p] = 0.0 d22G_tcc = self.d22G(r_pc[ij_t], r_pc[ik_t]) H_temp_t = (d2F_p[ij_t] * d22G_tcc.T).T for x in range(3): for y in range(3): H_temp_pcc[:, x, y] = -np.bincount( ik_t, weights=H_temp_t[:, x, y], minlength=nb_pairs) # H_temp_pcc[:, x, y] = np.bincount(tr_p[jk_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount(ij_t, weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount(tr_p[ik_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) H_pcc += H_temp_pcc d11G_tcc = self.d11G(r_pc[ij_t], r_pc[ik_t]) H_temp_t = (d2F_p[ij_t] * d11G_tcc.T).T for x in range(3): for y in range(3): H_temp_pcc[:, x, y] = -np.bincount( ij_t, weights=H_temp_t[:, x, y], minlength=nb_pairs) # H_temp_pcc[:, x, y] = np.bincount(tr_p[jk_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount(ij_t, weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount(tr_p[ik_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) H_pcc += H_temp_pcc Hxx_p = +H_pcc[:, 0, 0] Hyy_p = +H_pcc[:, 1, 1] Hzz_p = +H_pcc[:, 2, 2] Hyz_p = +H_pcc[:, 1, 2] Hxz_p = +H_pcc[:, 0, 2] Hxy_p = +H_pcc[:, 0, 1] Hzy_p = +H_pcc[:, 2, 1] Hzx_p = +H_pcc[:, 2, 0] Hyx_p = +H_pcc[:, 1, 0] d1x2xG_t = self.d1x2xG(r_pc[ij_t], r_pc[ik_t]) d1y2yG_t = self.d1y2yG(r_pc[ij_t], r_pc[ik_t]) d1z2zG_t = self.d1z2zG(r_pc[ij_t], r_pc[ik_t]) d1y2zG_t = self.d1y2zG(r_pc[ij_t], r_pc[ik_t]) d1x2zG_t = self.d1x2zG(r_pc[ij_t], r_pc[ik_t]) d1x2yG_t = self.d1x2yG(r_pc[ij_t], r_pc[ik_t]) d1z2yG_t = self.d1z2yG(r_pc[ij_t], r_pc[ik_t]) d1z2xG_t = self.d1z2xG(r_pc[ij_t], r_pc[ik_t]) d1y2xG_t = self.d1y2xG(r_pc[ij_t], r_pc[ik_t]) Hxx_t = d2F_p[ij_t] * d1x2xG_t Hyy_t = d2F_p[ij_t] * d1y2yG_t Hzz_t = d2F_p[ij_t] * d1z2zG_t Hyz_t = d2F_p[ij_t] * d1y2zG_t Hxz_t = d2F_p[ij_t] * d1x2zG_t Hxy_t = d2F_p[ij_t] * d1x2yG_t Hzy_t = d2F_p[ij_t] * d1z2yG_t Hzx_t = d2F_p[ij_t] * d1z2xG_t Hyx_t = d2F_p[ij_t] * d1y2xG_t Hxx_p += np.bincount( jk_t, weights=Hxx_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hxx_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hxx_t, minlength=nb_pairs) Hyy_p += np.bincount( jk_t, weights=Hyy_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hyy_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hyy_t, minlength=nb_pairs) Hzz_p += np.bincount( jk_t, weights=Hzz_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hzz_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hzz_t, minlength=nb_pairs) Hyz_p += np.bincount( jk_t, weights=Hyz_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hyz_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hyz_t, minlength=nb_pairs) Hxz_p += np.bincount( jk_t, weights=Hxz_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hxz_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hxz_t, minlength=nb_pairs) Hxy_p += np.bincount( jk_t, weights=Hxy_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hxy_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hxy_t, minlength=nb_pairs) Hzy_p += np.bincount( jk_t, weights=Hzy_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hzy_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hzy_t, minlength=nb_pairs) Hzx_p += np.bincount( jk_t, weights=Hzx_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hzx_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hzx_t, minlength=nb_pairs) Hyx_p += np.bincount( jk_t, weights=Hyx_t, minlength=nb_pairs) - np.bincount( tr_p[ij_t], weights=Hyx_t, minlength=nb_pairs) - np.bincount( ik_t, weights=Hyx_t, minlength=nb_pairs) # Hessian term #3 ## Terms involving D_1 * D_1 d1G_t = self.d1G(r_pc[ij_t], r_pc[ik_t]) d22F_p = self.d22F(r_p, xi_p) d22F_p[mask_p] = 0.0 d1xG_p = np.bincount(ij_t, weights=d1G_t[:, 0], minlength=nb_pairs) d1yG_p = np.bincount(ij_t, weights=d1G_t[:, 1], minlength=nb_pairs) d1zG_p = np.bincount(ij_t, weights=d1G_t[:, 2], minlength=nb_pairs) d1G_p = np.transpose([d1xG_p, d1yG_p, d1zG_p]) H_pcc = -(d22F_p * (d1G_p.reshape(-1, 3, 1) * d1G_p.reshape(-1, 1, 3)).T).T ## Terms involving D_2 * D_2 d2G_t = self.d2G(r_pc[ij_t], r_pc[ik_t]) d2xG_p = np.bincount(ij_t, weights=d2G_t[:, 0], minlength=nb_pairs) d2yG_p = np.bincount(ij_t, weights=d2G_t[:, 1], minlength=nb_pairs) d2zG_p = np.bincount(ij_t, weights=d2G_t[:, 2], minlength=nb_pairs) d2G_p = np.transpose([d2xG_p, d2yG_p, d2zG_p]) Q = ((d22F_p * d2G_p.T).T[ij_t].reshape(-1, 3, 1) * d2G_t.reshape(-1, 1, 3)) H_temp_pcc = np.zeros_like(H_temp_pcc) for x in range(3): for y in range(3): H_temp_pcc[:, x, y] = -np.bincount( ik_t, weights=Q[:, x, y], minlength=nb_pairs) # H_temp_pcc[:, x, y] = np.bincount(tr_p[jk_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount(ij_t, weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount(tr_p[ik_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) H_pcc += H_temp_pcc Hxx_p += H_pcc[:, 0, 0] Hyy_p += H_pcc[:, 1, 1] Hzz_p += H_pcc[:, 2, 2] Hyz_p += H_pcc[:, 1, 2] Hxz_p += H_pcc[:, 0, 2] Hxy_p += H_pcc[:, 0, 1] Hzy_p += H_pcc[:, 2, 1] Hzx_p += H_pcc[:, 2, 0] Hyx_p += H_pcc[:, 1, 0] for il_im in range(nb_triplets): il = ij_t[il_im] im = ik_t[il_im] lm = jk_t[il_im] for t in range(first_ij[il], first_ij[il + 1]): ij = ik_t[t] if ij != il and ij != im: r_p_ij = np.array([r_pc[ij]]) r_p_il = np.array([r_pc[il]]) r_p_im = np.array([r_pc[im]]) H_pcc = (0.5 * d22F_p[ij] * (self.d2G(r_p_ij, r_p_il).reshape(-1, 3, 1) * self.d2G(r_p_ij, r_p_im).reshape(-1, 1, 3)).T).T Hxx_p[lm] += H_pcc[:, 0, 0] Hyy_p[lm] += H_pcc[:, 1, 1] Hzz_p[lm] += H_pcc[:, 2, 2] Hyz_p[lm] += H_pcc[:, 1, 2] Hxz_p[lm] += H_pcc[:, 0, 2] Hxy_p[lm] += H_pcc[:, 0, 1] Hzy_p[lm] += H_pcc[:, 2, 1] Hzx_p[lm] += H_pcc[:, 2, 0] Hyx_p[lm] += H_pcc[:, 1, 0] ## Terms involving D_1 * D_2 # Was d2*d1 Q = ((d22F_p * d1G_p.T).T[ij_t].reshape(-1, 3, 1) * d2G_t.reshape(-1, 1, 3)) H_temp_pcc = np.zeros_like(H_temp_pcc) for x in range(3): for y in range(3): H_temp_pcc[:, x, y] = np.bincount( jk_t, weights=Q[:, x, y], minlength=nb_pairs) - np.bincount( ik_t, weights=Q[:, x, y], minlength=nb_pairs) # H_temp_pcc[:, x, y] = np.bincount(tr_p[jk_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount(ij_t, weights=H_temp_t[:, x, y], minlength=nb_pairs) - np.bincount(tr_p[ik_t], weights=H_temp_t[:, x, y], minlength=nb_pairs) H_pcc = H_temp_pcc H_pcc -= (d22F_p * (d2G_p.reshape(-1, 3, 1) * d1G_p.reshape(-1, 1, 3)).T).T Hxx_p += H_pcc[:, 0, 0] Hyy_p += H_pcc[:, 1, 1] Hzz_p += H_pcc[:, 2, 2] Hyz_p += H_pcc[:, 1, 2] Hxz_p += H_pcc[:, 0, 2] Hxy_p += H_pcc[:, 0, 1] Hzy_p += H_pcc[:, 2, 1] Hzx_p += H_pcc[:, 2, 0] Hyx_p += H_pcc[:, 1, 0] # Add the conjugate terms (symmetrize Hessian) Hxx_p += Hxx_p[tr_p] Hyy_p += Hyy_p[tr_p] Hzz_p += Hzz_p[tr_p] tmp = Hyz_p.copy() Hyz_p += Hzy_p[tr_p] Hzy_p += tmp[tr_p] tmp = Hxz_p.copy() Hxz_p += Hzx_p[tr_p] Hzx_p += tmp[tr_p] tmp = Hxy_p.copy() Hxy_p += Hyx_p[tr_p] Hyx_p += tmp[tr_p] # Construct diagonal terms from off-diagonal terms Hxx_a = -np.bincount(i_p, weights=Hxx_p) Hyy_a = -np.bincount(i_p, weights=Hyy_p) Hzz_a = -np.bincount(i_p, weights=Hzz_p) Hyz_a = -np.bincount(i_p, weights=Hyz_p) Hxz_a = -np.bincount(i_p, weights=Hxz_p) Hxy_a = -np.bincount(i_p, weights=Hxy_p) Hzy_a = -np.bincount(i_p, weights=Hzy_p) Hzx_a = -np.bincount(i_p, weights=Hzx_p) Hyx_a = -np.bincount(i_p, weights=Hyx_p) # Construct full off-diagonal term H_pcc = np.transpose([[Hxx_p, Hyx_p, Hzx_p], [Hxy_p, Hyy_p, Hzy_p], [Hxz_p, Hyz_p, Hzz_p]]) # Construct full diagonal term H_acc = np.transpose([[Hxx_a, Hxy_a, Hxz_a], [Hyx_a, Hyy_a, Hyz_a], [Hzx_a, Hzy_a, Hzz_a]]) if divide_by_masses: mass_nat = atoms.get_masses() geom_mean_mass_n = np.sqrt(mass_nat[i_p] * mass_nat[j_p]) return \ bsr_matrix(((H_pcc.T/(2 * geom_mean_mass_n)).T, j_p, first_i), shape=(3*nb_atoms, 3*nb_atoms)) \ + bsr_matrix(((H_acc.T/(2 * mass_nat)).T, np.arange(nb_atoms), np.arange(nb_atoms+1)), shape=(3*nb_atoms, 3*nb_atoms)) else: return \ bsr_matrix((H_pcc/2, j_p, first_i), shape=(3*nb_atoms, 3*nb_atoms)) \ + bsr_matrix((H_acc/2, np.arange(nb_atoms), np.arange(nb_atoms+1)), shape=(3*nb_atoms, 3*nb_atoms))
def test_first_neighbours(self): i = [1,1,1,1,3,3,3] self.assertArrayAlmostEqual(first_neighbours(5, i), [-1,0,-1,4,-1,7]) i = [0,1,2,3,4,5] self.assertArrayAlmostEqual(first_neighbours(6, i), [0,1,2,3,4,5,6])