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_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 energy_virial_and_forces(self, atomic_numbers_i, i_n, j_n, dr_nc, abs_dr_n): """ Compute the potential energy, the virial and the forces. Parameters ---------- atomic_numbers_i : array_like Atomic number for each atom in the system i_n, j_n : array_like Neighbor pairs dr_nc : array_like Distance vectors between neighbors abd_dr_n : array_like Length of distance vectors between neighbors Returns ------- epot : float Potential energy virial_v : array Virial forces_ic : array Forces acting on each atom """ nat = len(atomic_numbers_i) atnums_in_system = set(atomic_numbers_i) for atnum in atnums_in_system: if atnum not in self._db_atomic_numbers: raise RuntimeError('Element with atomic number {} found, but ' 'this atomic number has no EAM ' 'parametrization'.format(atnum)) # Density f_n = np.zeros_like(abs_dr_n) df_n = np.zeros_like(abs_dr_n) for atidx1, atnum1 in enumerate(self._db_atomic_numbers): f1 = self.f[atidx1] df1 = self.df[atidx1] mask1 = atomic_numbers_i[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] mask = np.logical_and(mask1, atomic_numbers_i[i_n] == atnum2) if mask.sum() > 0: f_n[mask] = f(abs_dr_n[mask]) df_n[mask] = df(abs_dr_n[mask]) else: f_n[mask1] = f1(abs_dr_n[mask1]) df_n[mask1] = df1(abs_dr_n[mask1]) density_i = np.bincount(i_n, weights=f_n, minlength=nat) # Repulsion rep_n = np.zeros_like(abs_dr_n) drep_n = np.zeros_like(abs_dr_n) for atidx1, atnum1 in enumerate(self._db_atomic_numbers): rep1 = self.rep[atidx1] drep1 = self.drep[atidx1] mask1 = atomic_numbers_i[i_n] == atnum1 if mask1.sum() > 0: for atidx2, atnum2 in enumerate(self._db_atomic_numbers): rep = rep1[atidx2] drep = drep1[atidx2] mask = np.logical_and(mask1, atomic_numbers_i[j_n] == atnum2) if mask.sum() > 0: r = rep(abs_dr_n[mask]) / abs_dr_n[mask] rep_n[mask] = r drep_n[mask] = (drep(abs_dr_n[mask]) - r) / abs_dr_n[mask] # Energy epot = 0.5 * np.sum(rep_n) demb_i = np.zeros(nat) for atidx, atnum in enumerate(self._db_atomic_numbers): F = self.F[atidx] dF = self.dF[atidx] mask = atomic_numbers_i == atnum if mask.sum() > 0: epot += np.sum(F(density_i[mask])) demb_i[mask] += dF(density_i[mask]) # Forces reverse = find_indices_of_reversed_pairs(i_n, j_n, abs_dr_n) df_i_n = np.take(df_n, reverse) df_nc = -0.5 * ( (demb_i[i_n] * df_n + demb_i[j_n] * df_i_n) + drep_n).reshape( -1, 1) * dr_nc / abs_dr_n.reshape(-1, 1) # Sum for each atom fx_i = np.bincount(j_n, weights=df_nc[:,0], minlength=nat) - \ np.bincount(i_n, weights=df_nc[:,0], minlength=nat) fy_i = np.bincount(j_n, weights=df_nc[:,1], minlength=nat) - \ np.bincount(i_n, weights=df_nc[:,1], minlength=nat) fz_i = np.bincount(j_n, weights=df_nc[:,2], minlength=nat) - \ np.bincount(i_n, weights=df_nc[:,2], minlength=nat) # Virial virial_v = -np.array([ dr_nc[:, 0] * df_nc[:, 0], # xx dr_nc[:, 1] * df_nc[:, 1], # yy dr_nc[:, 2] * df_nc[:, 2], # zz dr_nc[:, 1] * df_nc[:, 2], # yz dr_nc[:, 0] * df_nc[:, 2], # xz dr_nc[:, 0] * df_nc[:, 1] ]).sum(axis=1) # xy return epot, virial_v, np.transpose([fx_i, fy_i, fz_i])
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))