def update_mw_down_step(self): w, v = np.linalg.eigh(self.mw_hessian) self.kill_modes = v[:, self.kill_inds] nus = eigval_to_wavenumber(w) assert all(nus[self.kill_inds] < self.nu_thresh), \ "ModeKill is intended for removal of imaginary frequencies " \ f"below {self.nu_thresh} cm⁻¹! The specified indices " \ f"{self.kill_inds} contain modes with positive frequencies " \ f"({nus[self.kill_inds]} cm⁻¹). Please choose different kill_inds!" """After diagonalization of the mass-weighted hessian the signs of the eigenvectors are arbitrary. We determine the correct sign of the eigenvector(s) from its overlap(s) with the gradient. To decrease the overall energy we have to step against the gradient, so the overlap of gradient and the respective eigen- vector(s) must be negative. If an overlap is positive we flip the sign of the corresponding eigenvector. """ mw_grad = self.mw_gradient mw_grad_normed = mw_grad / np.linalg.norm(mw_grad) overlaps = np.einsum("ij,i->j", self.kill_modes, mw_grad_normed) self.log("Overlaps between gradient and eigenvectors:") self.log(overlaps) flip = overlaps > 0 self.log("Eigenvector signs to be flipped:") self.log(str(flip)) self.kill_modes[:, flip] *= -1 # Create the step as the sum of the downhill steps along the modes # to remove. self.mw_down_step = (self.step_length * self.kill_modes).sum(axis=1)
def save_hessian(h5_fn, geom, cart_hessian=None, energy=None, mult=None): if cart_hessian is None: cart_hessian = geom.cart_hessian if energy is None: energy = geom.energy if mult is None: mult = geom.calculator.mult hessian = geom.eckart_projection(geom.mass_weigh_hessian(cart_hessian)) eigvals, eigvecs = np.linalg.eigh(hessian) vibfreqs = eigval_to_wavenumber(eigvals) masses = geom.masses atoms = geom.atoms coords3d = geom.coords3d with h5py.File(h5_fn, "w") as handle: handle.create_dataset("hessian", data=cart_hessian) handle.create_dataset("vibfreqs", data=vibfreqs) handle.create_dataset("masses", data=masses) handle.create_dataset("coords3d", data=coords3d) handle.attrs["atoms"] = atoms handle.attrs["energy"] = energy handle.attrs["mult"] = mult
def imag_modes_from_geom(geom, freq_thresh=-10, points=10, displ=None): NormalMode = namedtuple("NormalMode", "nu mode trj_str") # We don't want to do start any calculation here, so we directly access # the attribute underlying the geom.hessian property. mw_H = geom.eckart_projection(geom.mass_weigh_hessian(geom._hessian)) eigvals, eigvecs = np.linalg.eigh(mw_H) nus = eigval_to_wavenumber(eigvals) below_thresh = nus < freq_thresh imag_modes = list() for nu, eigvec in zip(nus[below_thresh], eigvecs[:, below_thresh].T): comment = f"{nu:.2f} cm⁻¹" trj_str = get_tangent_trj_str(geom.atoms, geom.cart_coords, eigvec, comment=comment, points=points, displ=displ) imag_modes.append(NormalMode( nu=nu, mode=eigvec, trj_str=trj_str, )) return imag_modes
def test_hessian(azetidine): H = azetidine.mw_hessian H = azetidine.eckart_projection(H) w, v = np.linalg.eigh(H) nus = eigval_to_wavenumber(w) ref_nus = np.array((3278.33113, 3320.16889, 3714.42723)) np.testing.assert_allclose(nus[-3:], ref_nus, atol=1e-4)
def test_xtb_hessian(geom): Hmw = geom.mw_hessian Hmw = geom.eckart_projection(Hmw) w, v = np.linalg.eigh(Hmw) nus = eigval_to_wavenumber(w) assert nus[0] == pytest.approx(-571.77084564) assert nus[-1] == pytest.approx(1554.94377536)
def test_modekill_pyscf(this_dir): fn = this_dir / "ethane_shaked.xyz" geom = geom_loader(fn) calc = PySCF(basis="sto3g", xc="bp86", pal=2) geom.set_calculator(calc) w, v = np.linalg.eigh(geom.eckart_projection(geom.mw_hessian)) nus = eigval_to_wavenumber(w) assert nus[0] == pytest.approx(-266.11360383) modekill = ModeKill(geom, kill_inds=[ 0, ]) modekill.run() assert modekill.converged w, v = np.linalg.eigh(geom.eckart_projection(geom.mw_hessian)) nus = eigval_to_wavenumber(w) assert nus[0] == pytest.approx(-4.5086469e-05, abs=1e-4)
def test_modekill_xtb(this_dir): fn = this_dir / "shaked.geom_000.xyz" geom = geom_loader(fn) calc = XTB(pal=4) geom.set_calculator(calc) w, v = np.linalg.eigh(geom.mw_hessian) nus = eigval_to_wavenumber(w) assert nus[0] == pytest.approx(-199.5586146) assert nus[1] == pytest.approx(-94.01553723) modekill = ModeKill(geom, kill_inds=[ 0, ]) modekill.run() w, v = np.linalg.eigh(geom.mw_hessian) nus = eigval_to_wavenumber(w) assert nus[0] == pytest.approx(-96.31027310)
def get_imag_frequencies(self, hessian=None, thresh=1e-6): if hessian is None: hessian = self.cart_hessian mw_hessian = self.mass_weigh_hessian(hessian) proj_hessian = self.eckart_projection(mw_hessian) eigvals, eigvecs = np.linalg.eigh(proj_hessian) neg_inds = eigvals < thresh neg_eigvals = eigvals[neg_inds] return eigval_to_wavenumber(neg_eigvals)
def step(self): if self.hessian_update and (self.cur_cycle > 0): # Hessian update with mass-weighted values dx = self.irc_mw_coords[-1] - self.irc_mw_coords[-2] dg = (self.irc_mw_gradients[-1] - self.irc_mw_gradients[-2]) d_mw_H, key = bofill_update(self.mw_hessian, dx, dg) self.mw_hessian += d_mw_H # norm(dx) is probably self.step_length ;) norm_dx = np.linalg.norm(dx) norm_dg = np.linalg.norm(dg) self.log(f"Did {key} hessian update: norm(dx)={norm_dx:.4e}, " f"norm(dg)={norm_dg:.4e}.") else: # Recalculate exact hessian self.mw_hessian = self.geometry.mw_hessian self.log("Recalculated exact hessian.") w, v = np.linalg.eigh(self.mw_hessian) # Overlaps between current normal modes and the modes we want to # remove. overlaps = np.abs(np.einsum("ij,ik->jk", self.kill_modes, v)) self.log(f"Overlaps between original modes and current modes:") # overlaps contains one row per mode to remove for i, ovlps in enumerate(overlaps): above_thresh = ovlps > self.ovlp_thresh ovlp_str = " ".join([ f"{i:02d}: {o:.4f}" for i, o in zip(self.indices[above_thresh], ovlps[above_thresh]) ]) self.log(f"Org. mode {i:02d}:\n\t\t\t{ovlp_str}") nus = eigval_to_wavenumber(w) neg_inds = nus <= self.nu_thresh neg_nus = nus[neg_inds] self.neg_nus.append(neg_nus) self.log(f"Wavenumbers of imaginary modes (<= {self.nu_thresh} cm⁻¹):") self.log(f"{neg_nus} cm⁻¹") # Check if any eigenvalues became positive. If so remove them and update # the step. If no mode to remove is left we are finished and can signal # convergence. argmax = overlaps.argmax(axis=1) eigvals = w[argmax] pos_eigvals = eigvals > 0 if any(pos_eigvals): # Only keep negative eigenvalues. flipped = self.kill_inds[pos_eigvals] self.kill_inds = self.kill_inds[~pos_eigvals] self.update_mw_down_step() self.log(f"Eigenvalue(s) of mode(s) {flipped} became positive!") self.converged = len(self.kill_inds) == 0 if not self.converged: self.mw_coords += self.mw_down_step
def davidson( geom, q, trial_step_size=0.01, hessian_precon=None, max_cycles=25, res_rms_thresh=1e-4, ): if hessian_precon is not None: print("Using supplied Hessians as preconditioner.") B_list = list() S_list = list() msqrt = np.sqrt(geom.masses_rep) # Projector to remove translation and rotation P = get_trans_rot_projector(geom.cart_coords, geom.masses) l_proj = P.dot(q.l_mw) / msqrt q = NormalMode(l_proj, geom.masses_rep) b_prev = q.l_mw for i in range(max_cycles): print(f"Cycle {i:02d}") b = q.l_mw B_list.append(b) B = np.stack(B_list, axis=1) # Overlaps of basis vectors in B # B_ovlp = np.einsum("ij,kj->ik", B_list, B_list) # print("Basis vector overlaps") # print(B_ovlp) # Estimate action of Hessian on basis vector by finite differences # Get step size in mass-weighted coordinates that results # in the desired 'trial_step_size' in not-mass-weighted coordinates. mw_step_size = q.mw_norm_for_norm(trial_step_size) # Actual step in non-mass-weighted coordinates step = trial_step_size * q.l s = fin_diff(geom, step, mw_step_size) S_list.append(s) S = np.stack(S_list, axis=1) # Calculate and symmetrize approximate hessian Hm_ = B.T.dot(S) Hm_ = (Hm_ + Hm_.T) / 2 # Diagonalization v, w = np.linalg.eigh(Hm_) # i-th approximation to exact eigenvector approx_modes = (w * B[:, :, None]).sum(axis=1).T # Calculate overlaps between previous root and the new approximate # normal modes for root following. mode_overlaps = (approx_modes * b_prev).sum(axis=1) mode_ind = np.abs(mode_overlaps).argmax() print(f"\tFollowing mode {mode_ind}") residues = list() for s in range(i + 1): residues.append((w[:, s] * (S - v[s] * B)).sum(axis=1)) residues = np.array(residues) b_prev = approx_modes[mode_ind] # Construct new basis vector from residuum of selected mode if hessian_precon is not None: # Construct X X = np.linalg.inv( hessian_precon - v[mode_ind] * np.eye(hessian_precon.shape[0]) ) b = X.dot(residues[mode_ind]) else: b = residues[mode_ind] # Project out translation and rotation from new mode guess b = P.dot(b) # Orthogonalize new mode against current basis vectors rows, cols = B.shape B_ = np.zeros((rows, cols + 1)) B_[:, :cols] = B B_[:, -1] = b b, _ = np.linalg.qr(B_) # New NormalMode from non-mass-weighted displacements q = NormalMode(b[:, -1] / msqrt, geom.masses_rep) # Calculate wavenumbers nus = eigval_to_wavenumber(v) # Check convergence criteria max_res = np.abs(residues).max(axis=1) res_rms = np.sqrt(np.mean(residues ** 2, axis=1)) # Print progress print("\t # | wavelength | rms | max") for j, (nu, rms, mr) in enumerate(zip(nus, res_rms, max_res)): sel_str = "*" if (i == mode_ind) else " " print(f"\t{j:02d}{sel_str} | {nu:> 16.2f} cm⁻¹ | {rms:.8f} | {mr:.8f}") print() if res_rms[mode_ind] < res_rms_thresh: print("Converged!") break result = DavidsonResult( cur_cycle=i, nus=nus, mode_ind=mode_ind, ) return result
def do_final_hessian(geom, save_hessian=True, write_imag_modes=False, prefix=""): print(highlight_text("Hessian at final geometry", level=1)) print() # TODO: Add cartesian_hessian property to Geometry to avoid # accessing a "private" attribute. hessian = geom.cart_hessian print("... mass-weighing cartesian hessian") mw_hessian = geom.mass_weigh_hessian(hessian) print("... doing Eckart-projection") proj_hessian = geom.eckart_projection(mw_hessian) eigvals, eigvecs = np.linalg.eigh(proj_hessian) ev_thresh = -1e-6 neg_inds = eigvals < ev_thresh neg_eigvals = eigvals[neg_inds] neg_num = sum(neg_inds) eigval_str = np.array2string(eigvals[:10], precision=4) print() print("First 10 eigenvalues", eigval_str) # print(f"Self found {neg_num} eigenvalue(s) < {ev_thresh}.") if neg_num > 0: wavenumbers = eigval_to_wavenumber(neg_eigvals) wavenum_str = np.array2string(wavenumbers, precision=2) print("Imaginary frequencies:", wavenum_str, "cm⁻¹") if prefix: prefix = f"{prefix}_" if save_hessian: final_hessian_fn = prefix + "calculated_final_cart_hessian" np.savetxt(final_hessian_fn, hessian) print() print( f"Wrote final (not mass-weighted) hessian to '{final_hessian_fn}'." ) # Also write HD5 hessian final_h5_hessian_fn = prefix + "final_hessian.h5" save_h5_hessian(final_h5_hessian_fn, geom) print(f"Wrote HD5 Hessian to '{final_h5_hessian_fn}'.") imag_fns = list() if write_imag_modes: imag_modes = imag_modes_from_geom(geom) for i, imag_mode in enumerate(imag_modes): trj_fn = prefix + f"imaginary_mode_{i:03d}.trj" imag_fns.append(trj_fn) with open(trj_fn, "w") as handle: handle.write(imag_mode.trj_str) print( f"Wrote imaginary mode with ṽ={imag_mode.nu:.2f} cm⁻¹ to '{trj_fn}'" ) res = FinalHessianResult( neg_eigvals=neg_eigvals, eigvals=eigvals, nus=eigval_to_wavenumber(eigvals), imag_fns=imag_fns, ) return res
def block_davidson( cart_coords, masses, forces_getter, guess_modes, lowest=None, trial_step_size=0.01, hessian_precon=None, max_cycles=25, res_rms_thresh=1e-4, start_precon=5, remove_trans_rot=True, print_level=1, ): num = len(guess_modes) B_full = np.zeros((len(guess_modes[0]), num * max_cycles)) S_full = np.zeros_like(B_full) I = np.eye(cart_coords.size) masses_rep = np.repeat(masses, 3) msqrt = np.sqrt(masses_rep) # Projector to remove translation and rotation P = get_trans_rot_projector(cart_coords, masses) guess_modes = [ NormalMode(P.dot(mode.l_mw) / msqrt, masses_rep) for mode in guess_modes ] col_fmts = "int int int float float str".split() header = ("#", "subspace size", "mode", "ṽ / cm⁻¹", "rms(r)", "Conv") table = TablePrinter(header, col_fmts, width=11) if print_level == 1: table.print_header() b_prev = np.array([mode.l_mw for mode in guess_modes]).T for i in range(max_cycles): # Add new basis vectors to B matrix b = np.array([mode.l_mw for mode in guess_modes]).T from_ = i * num to_ = (i + 1) * num B_full[:, from_:to_] = b # Estimate action of Hessian by finite differences. for j in range(num): mode = guess_modes[j] # Get a step size in mass-weighted coordinates that results # in the desired 'trial_step_size' in not-mass-weighted coordinates. mw_step_size = mode.mw_norm_for_norm(trial_step_size) # Actual step in non-mass-weighted coordinates step = trial_step_size * mode.l S_full[:, from_ + j] = ( # Convert to mass-weighted coordinates forces_fin_diff(forces_getter, cart_coords, step, mw_step_size) / msqrt) # Views on columns that are actually set B = B_full[:, :to_] S = S_full[:, :to_] # Calculate and symmetrize approximate hessian Hm = B.T.dot(S) Hm = (Hm + Hm.T) / 2 # Diagonalize small Hessian w, v = np.linalg.eigh(Hm) # Approximations to exact eigenvectors in current cycle approx_modes = (v * B[:, :, None]).sum(axis=1).T # Calculate overlaps between previous root and the new approximate # normal modes for root following. if lowest is None: # 2D overlap array. approx_modes in row, b_prev in columns. overlaps = np.einsum("ij,jk->ik", approx_modes, b_prev) mode_inds = np.abs(overlaps).argmax(axis=0) else: mode_inds = np.arange(lowest) b_prev = approx_modes[mode_inds].T # Eq. (7) in [1] residues = (v * (S[:, :, None] - w * B[:, :, None])).sum(axis=1) # Determine preconditioner matrix # # Use supplied matrix if hessian_precon is not None: precon_mat = hessian_precon # Reconstruct Hessian, but only start after some cycles elif i >= start_precon: precon_mat = B.dot(Hm).dot(B.T) # No preconditioning if no matrix was supplied or we are in an early cycle. else: precon_mat = None # Construct new basis vector from residuum of selected mode b = np.zeros_like(b_prev) for j, mode_ind in enumerate(mode_inds): r = residues[:, mode_ind] if precon_mat is not None: # Construct actual preconditioner X X = np.linalg.pinv(precon_mat - w[mode_ind] * I, rcond=1e-8) b[:, j] = X.dot(r) else: b[:, j] = r # Project out translation and rotation from new mode guess if remove_trans_rot: b = P.dot(b) # Orthogonalize new vectors against preset vectors b = np.linalg.qr(np.concatenate((B, b), axis=1))[0][:, -num:] # New NormalMode from non-mass-weighted displacements guess_modes = [NormalMode(b_ / msqrt, masses_rep) for b_ in b.T] # Calculate wavenumbers nus = eigval_to_wavenumber(w) # Check convergence criteria max_res = np.abs(residues).max(axis=0) res_rms = np.sqrt(np.mean(residues**2, axis=0)) converged = res_rms < res_rms_thresh # Print progress if requested if print_level == 2: print(f"Cycle {i:02d}") print("\t # | ṽ / cm⁻¹| rms(r) | max(|r|) ") print("\t------------------------------------------") for j, (nu, rms, mr) in enumerate(zip(nus, res_rms, max_res)): sel_str = "*" if (j in mode_inds) else " " conv_str = "✓" if converged[j] else "" print( f"\t{j:02d}{sel_str} | {nu:> 10.2f} | {rms:.8f} | {mr:.8f} {conv_str}" ) print() elif print_level == 1: for j in mode_inds: conv_str = "✓" if converged[j] else "✗" table.print_row( (i, B.shape[1], j, nus[j], res_rms[j], conv_str)) # Convergence is signalled using only the roots we are actually interested in modes_converged = all(converged[mode_inds]) if modes_converged: if print_level > 0: print(f"\tDavidson procedure converged in {i+1} cycles!") if lowest is not None: nus_str = np.array2string(nus[mode_inds], precision=2) print(f"\tLowest {lowest} wavenumbers: {nus_str} cm⁻¹") neg_nus = sum(nus[mode_inds] < 0) type_ = "minimum" if ( neg_nus == 0) else f"saddle point of index {neg_nus}" print(f"\tThis geometry seems to be a {type_} on the PES.") break result = DavidsonResult( cur_cycle=i, converged=modes_converged, final_modes=guess_modes, qs=approx_modes, nus=nus, mode_inds=mode_inds, res_rms=res_rms, ) return result
def optimize(self): grad = self.geometry.gradient self.forces.append(-grad.copy()) self.energies.append(self.geometry.energy) if self.cur_cycle > 0: self.update_trust_radius() self.update_hessian() # eigvals_org, eigvecs_org = np.linalg.eigh(self.H) # grad_trans = eigvals_org.T.dot(grad) # Mass-weighted hessian H_mw = self.geometry.mass_weigh_hessian(self.H) # Project out translation and rotation if self.geometry.coords.size > 3: H_mw = self.geometry.eckart_projection(H_mw) eigvals, eigvecs = np.linalg.eigh(H_mw) # Drop translational/rotational modes as they will have # small (zero) eigenvalues. Zero the corresponding gradient entries. eigvals, eigvecs, small_inds = self.filter_small_eigvals(eigvals, eigvecs, mask=True) wavenumbers = eigval_to_wavenumber(eigvals) freeze_inds = np.zeros_like(wavenumbers, dtype=bool) if self.freeze_modes: freeze_inds = wavenumbers < self.freeze_modes eigvals = eigvals[~freeze_inds] eigvecs = eigvecs[:, ~freeze_inds] self.log(f"{np.sum(freeze_inds)} normal modes will be frozen.") frozen_str = ["(frozen)" if frozen else "" for frozen in freeze_inds] wavenumber_str = "\n".join([ f"\t{i:> 3d}: {wn:> 8.2f} cm⁻¹ {frz}" for i, (wn, frz) in enumerate(zip(wavenumbers, frozen_str)) ]) self.log("Frequencies:\n" + wavenumber_str) # Transform gradient to eigensystem of Hessian. We also have to use # the mass-weighted gradient. grad_q = eigvecs.T.dot(self.geometry.mw_gradient) mw_H_aug = self.get_augmented_hessian(eigvals, grad_q) # Discard eigenvector for now mw_step, eigval, nu, _ = self.solve_rfo(mw_H_aug, "min") # Transform back to original basis. Right now the step is still in # mass-weighted coordinates. mw_step = eigvecs @ mw_step # Un-massweigh step step = mw_step / self.sqrt_m step_norm = np.linalg.norm(step) if step_norm > self.trust_radius: step = step / step_norm * self.trust_radius if self.freeze_modes: # With frozen modes we only want to consider the gradient contributions # from non-frozen modes. So here we transform back the gradient and # un-weigh it. grad = eigvecs.dot(grad_q) * self.sqrt_m self.modified_forces.append(-grad) quadratic_prediction = step @ grad + 0.5 * step @ self.H @ step rfo_prediction = quadratic_prediction / (1 + step @ step) self.predicted_energy_changes.append(rfo_prediction) return step