Пример #1
0
    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)
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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)
Пример #5
0
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)
Пример #6
0
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)
Пример #7
0
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)
Пример #8
0
    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)
Пример #9
0
    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
Пример #10
0
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
Пример #11
0
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
Пример #12
0
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
Пример #13
0
    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