示例#1
0
    def get_step_func(self, eigvals, gradient, grad_rms_thresh=1e-2):
        positive_definite = (eigvals < 0).sum() == 0
        gradient_small = rms(gradient) < grad_rms_thresh

        if self.adapt_step_func and gradient_small and positive_definite:
            return self.get_newton_step_on_trust, self.quadratic_model
        # RFO fallback
        else:
            return self.get_rs_step, self.rfo_model
示例#2
0
def dimer_method(geom0,
                 N,
                 R,
                 calc_getter,
                 max_cycles=50,
                 f_thresh=1e-3,
                 max_step=0.3,
                 rot_kwargs=None,
                 trans_kwargs=None):
    if rot_kwargs is None:
        rot_kwargs = {}
    if trans_kwargs is None:
        trans_kwargs = {}

    geom1, geom2 = get_dimer_ends(geom0, N, R, calc_getter)
    coords = [
        (geom1.coords, geom0.coords, geom2.coords),
    ]
    restrict_step = partial(restrict_step_length, max_length=max_step)
    trans_optimizer = lbfgs_closure(lambda _, *args: get_f_trans(*args),
                                    restrict_step=restrict_step)
    for i in range(max_cycles):
        f0 = geom0.forces
        f0_rms = rms(f0)
        print(f"{i:0d} rms(f0)={f0_rms:.6f}")
        if f0_rms < f_thresh:
            print("Converged!")
            break

        # Rotation
        rot_result = rotate_dimer(geom0, geom1, geom2, N, R, **rot_kwargs)
        geom1, geom2, N, C, _ = rot_result

        # Translation
        step, f_trans = trans_optimizer(geom0.coords, geom0, N, C)
        new_coords0 = geom0.coords + step
        geom0.coords = new_coords0

        # Steepet descent
        # f_trans = get_f_trans(geom0, N, C)
        # alpha = 0.5
        # step = alpha * f_trans
        # new_coords0 = geom0.coords + alpha*step
        # geom0.coords = new_coords0

        update_dimer_ends(geom0, geom1, geom2, N, R)

        coords.append((geom1.coords, geom0.coords, geom2.coords))
    # TODO: return dimerresults
    return coords
示例#3
0
    def do_dimer_rotations(self, rotation_thresh=None):
        self.log("Doing dimer rotations")
        if rotation_thresh is None:
            rotation_thresh = self.rotation_thresh
            self.log(f"\tThreshold norm(rot_force)={rotation_thresh:.6f}")

        lbfgs = small_lbfgs_closure()
        try:
            N_first = self.N
            prev_step = None
            for i in range(
                    self.rotation_max_cycles):  # lgtm [py/redundant-else]
                N_cur = self.N
                rot_force = self.rot_force
                rms_rot_force = rms(rot_force)
                if self.should_bias_f1:
                    C_real = self.C
                    C_bias = -self.bias_rotation_a * (self.N.dot(
                        self.N_init))**2
                    C = C_real + C_bias
                    C_str = f"C={C: .6f}, C_real={C_real: .6f}, C_bias={C_bias: .6f}"
                else:
                    C_str = f"C={self.C: .6f}"
                self.log(
                    f"\t{i:02d}: rms(rot_force)={rms_rot_force:.6f} {C_str}")
                if rms_rot_force <= rotation_thresh:
                    self.log("\trms(rot_force) is below threshold!")
                    raise RotationConverged
                coords1_old = self.coords1
                self.rotation_method(lbfgs, prev_step)
                actual_step = self.coords1 - coords1_old
                prev_step = actual_step
                rot_deg = np.rad2deg(np.arccos(N_cur.dot(self.N)))
                self.log(f"\t\tRotated by {rot_deg:.1f}°")
            else:
                msg =  "\tDimer rotation did not converge in " \
                      f"{self.rotation_max_cycles}"
        except RotationConverged:
            msg = f"\tDimer rotation converged in {i+1} cycle(s)."
        self.log(msg)
        self.log("\tN after rotation:\n\t" + str(self.N))
        self.log()
        # Restrict to interval [-1,1] where arccos is defined
        rot_deg = np.rad2deg(
            np.arccos(max(min(N_first.dot(self.N), 1.0), -1.0)))
        self.log(f"\tRotated by {rot_deg:.1f}° w.r.t. the orientation "
                 "before rotation(s).")
示例#4
0
    def run(self):
        # Calculate data at TS and create backup
        self.ts_coords = self.coords.copy()
        self.ts_mw_coords = self.mw_coords.copy()
        print("Calculating energy and gradient at the TS")
        self.ts_gradient = self.gradient.copy()
        self.ts_mw_gradient = self.mw_gradient.copy()
        self.ts_energy = self.energy

        ts_grad_norm = np.linalg.norm(self.ts_gradient)
        ts_grad_max = np.abs(self.ts_gradient).max()
        ts_grad_rms = rms(self.ts_gradient)

        self.log("Transition state (TS):\n"
                 f"\tnorm(grad)={ts_grad_norm:.6f}\n"
                 f"\t max(grad)={ts_grad_max:.6f}\n"
                 f"\t rms(grad)={ts_grad_rms:.6f}")

        print("IRC length in mw. coords, max(|grad|) and rms(grad) in "
              "unweighted coordinates.")

        self.init_hessian, hess_str = get_guess_hessian(
            self.geometry,
            self.hessian_init,
            cart_gradient=self.ts_gradient,
            h5_fn=f"hess_init_irc.h5",
        )
        self.log(f"Initial hessian: {hess_str}")

        # For forward/backward runs from a TS we need an intial displacement,
        # calculated from the transition vector (imaginary mode) of the TS
        # hessian. If we need/want a Hessian for a downhill run from a
        # non-stationary point (with non-vanishing gradient) depends on the
        # actual IRC integrator (e.g. EulerPC and LQA need a Hessian).
        if not self.downhill:
            self.init_displ = self.initial_displacement()

        if self.forward:
            print("\n" + highlight_text("IRC - Forward") + "\n")
            self.irc("forward")
            self.set_data("forward")

        # Add TS/starting data
        self.all_energies.append(self.ts_energy)
        self.all_coords.append(self.ts_coords)
        self.all_gradients.append(self.ts_gradient)
        self.all_mw_coords.append(self.ts_mw_coords)
        self.all_mw_gradients.append(self.ts_mw_gradient)
        self.ts_index = len(self.all_energies) - 1

        if self.backward:
            print("\n" + highlight_text("IRC - Backward") + "\n")
            self.irc("backward")
            self.set_data("backward")

        if self.downhill:
            print("\n" + highlight_text("IRC - Downhill") + "\n")
            self.irc("downhill")
            self.set_data("downhill")

        self.all_mw_coords = np.array(self.all_mw_coords)
        self.all_energies = np.array(self.all_energies)
        self.postprocess()
        if not self.downhill:
            self.write_trj(".", "finished")

            # Dump the whole IRC to HDF5
            dump_fn = "finished_" + self.dump_fn
            self.dump_data(dump_fn, full=True)

        # Convert to arrays
        [
            setattr(self, name, np.array(getattr(self, name)))
            for name in "all_energies all_coords all_gradients "
            "all_mw_coords all_mw_gradients".split()
        ]

        # Right now self.all_mw_coords is still in mass-weighted coordinates.
        # Convert them to un-mass-weighted coordinates.
        self.all_mw_coords_umw = self.all_mw_coords / self.m_sqrt
示例#5
0
    def irc(self, direction):
        self.log(highlight_text(f"IRC {direction}", level=1))

        self.cur_direction = direction
        self.prepare(direction)
        # Calculate gradient
        self.gradient
        self.irc_energies.append(self.energy)
        # Non mass-weighted
        self.irc_coords.append(self.coords)
        self.irc_gradients.append(self.gradient)
        # Mass-weighted
        self.irc_mw_coords.append(self.mw_coords)
        self.irc_mw_gradients.append(self.mw_gradient)

        self.table.print_header()
        while True:
            self.log(highlight_text(f"IRC step {self.cur_cycle:03d}") + "\n")
            if self.cur_cycle == self.max_cycles:
                print("IRC steps exceeded. Stopping.")
                print()
                break

            # Do macroiteration/IRC step to update the geometry
            self.step()

            # Calculate gradient and energy on the new geometry
            # Non mass-weighted
            self.log("Calculating energy and gradient at new geometry.")
            self.irc_coords.append(self.coords)
            self.irc_gradients.append(self.gradient)
            self.irc_energies.append(self.energy)
            # Mass-weighted
            self.irc_mw_coords.append(self.mw_coords)
            self.irc_mw_gradients.append(self.mw_gradient)

            rms_grad = rms(self.gradient)

            # Only update once
            if not self.past_inflection:
                self.past_inflection = rms_grad >= self.rms_grad_thresh
                _ = "" if self.past_inflection else "not yet"
                self.log(f"(rms(grad) > threshold) {_} fullfilled!")

            irc_length = np.linalg.norm(self.irc_mw_coords[0] -
                                        self.irc_mw_coords[-1])
            dE = self.irc_energies[-1] - self.irc_energies[-2]
            max_grad = np.abs(self.gradient).max()

            row_args = (self.cur_cycle, irc_length, dE, max_grad, rms_grad)
            self.table.print_row(row_args)
            try:
                # The derived IRC classes may want to do some printing
                add_info = self.get_additional_print()
                self.table.print(add_info)
            except AttributeError:
                pass
            last_energy = self.irc_energies[-2]
            this_energy = self.irc_energies[-1]

            break_msg = ""
            if self.converged:
                break_msg = "Integrator indicated convergence!"
            elif self.past_inflection and (rms_grad <= self.rms_grad_thresh):
                break_msg = "rms(grad) converged!"
                self.converged = True
            # TODO: Allow some threshold?
            elif this_energy > last_energy:
                break_msg = "Energy increased!"
            elif abs(last_energy - this_energy) <= self.energy_thresh:
                break_msg = "Energy converged!"
                self.converged = True

            dumped = (self.cur_cycle % self.dump_every) == 0
            if dumped:
                dump_fn = f"{direction}_{self.dump_fn}"
                self.dump_data(dump_fn)

            if break_msg:
                self.table.print(break_msg)
                break

            self.cur_cycle += 1
            if check_for_stop_sign():
                break
            self.log("")
            sys.stdout.flush()

        if direction == "forward":
            self.irc_energies.reverse()
            self.irc_coords.reverse()
            self.irc_gradients.reverse()
            self.irc_mw_coords.reverse()
            self.irc_mw_gradients.reverse()

        if not dumped:
            self.dump_data(dump_fn)

        self.cur_direction = None
示例#6
0
    def optimize(self):
        energy, gradient, H, big_eigvals, big_eigvecs, resetted = self.housekeeping()

        # Reference RFO step, used for judging the proposed GDIIS step
        ref_gradient = gradient.copy()
        ref_rfo_step = self.get_rs_step(big_eigvals, big_eigvecs, gradient, name="RS-RFO")

        # Right everything is in place to check for convergence.  If all values are below
        # the thresholds, there is no need to do additional inter/extrapolations.
        if self.check_convergence(ref_rfo_step):
            self.log("Convergence achieved! Skipping inter/extrapolation.")
            return  ref_rfo_step


        # Try to interpolate an intermediate geometry, either from GDIIS or line search.
        #
        # Set some defaults
        ip_gradient = None
        ip_step = None
        diis_result = None

        # Check if we can do GDIIS or GEDIIS. If we (can) do a line search is decided
        # after trying GDIIS.
        rms_forces = rms(gradient)
        rms_step = rms(ref_rfo_step)
        can_diis = (rms_step <= self.gdiis_thresh) and (not resetted)
        can_gediis = (rms_forces <= self.gediis_thresh) and (not resetted)

        # GDIIS / GEDIIS, prefer GDIIS over GEDIIS
        if self.gdiis and can_diis:
            # Gradients as error vectors
            err_vecs = -np.array(self.forces)
            diis_result = gdiis(err_vecs, self.coords, self.forces, ref_rfo_step)
        # Don't try GEDIIS if GDIIS failed. If GEDIIS should be tried after GDIIS failed
        # comment the line below and uncomment the line following it.
        elif self.gediis and can_gediis:
        # if self.gediis and can_gediis and (diis_result == None):
            diis_result = gediis(self.coords, self.energies, self.forces, hessian=H)

        try:
            ip_coords = diis_result.coords
            ip_step = ip_coords - self.geometry.coords
            ip_gradient = -diis_result.forces
        # When diis_result is None
        except AttributeError:
            self.log("GDIIS didn't succeed.")

        # Try line search if GDIIS failed or not requested
        if self.line_search and (diis_result is None) and (not resetted):
            ip_energy, ip_gradient, ip_step = self.poly_line_search()

        # Use the interpolated gradient for the RFO step if interpolation succeeded
        if (ip_gradient is not None) and (ip_step is not None):
            gradient = ip_gradient
        # Keep the original gradient when the interpolation failed, but recreate
        # ip_step, as it will be returned as None from poly_line_search().
        else:
            ip_step = np.zeros_like(gradient)

        # RFO step (from intermediate geometry) with (interpolated) gradient
        rfo_step = self.get_rs_step(big_eigvals, big_eigvecs, gradient, name="RS-RFO")
        # Form full step. If we did not interpolate or it failed ip_step will be zero.
        step = rfo_step + ip_step

        # Use the original, actually calculated, gradient
        quadratic_prediction = step @ ref_gradient + 0.5 * step @ H @ step
        rfo_prediction = quadratic_prediction / (1 + step @ step)
        self.predicted_energy_changes.append(rfo_prediction)

        return step
示例#7
0
    def step(self):
        ##################
        # PREDICTOR STEP #
        ##################

        mw_grad = self.mw_gradient
        energy = self.energy

        if self.cur_cycle > 0:
            if self.hessian_recalc and (self.cur_cycle % self.hessian_recalc
                                        == 0):
                self.mw_hessian = self.geometry.mw_hessian
                h5_fn = f"hess_calc_irc_{self.direction}_cyc{self.cur_cycle}.h5"
                save_hessian(h5_fn, self.geometry)
                self.log("Calculated excact hessian")
            else:
                dx = self.mw_coords - self.irc_mw_coords[-2]
                dg = mw_grad - self.irc_mw_gradients[-2]
                dH, key = self.hessian_update_func(self.mw_hessian, dx, dg)
                self.mw_hessian += dH
                self.log(f"Did {key} hessian update before predictor step.")
            self.dwi.update(self.mw_coords.copy(), energy, mw_grad,
                            self.mw_hessian.copy())

        # Create a copy of the inital coordinates for the determination
        # of the actual step size in the predictor Euler integration.
        init_mw_coords = self.mw_coords.copy()

        get_integration_length = self.get_integration_length_func(
            init_mw_coords)

        # Calculate predictor Euler-integration step length. See get_conv_fact
        # method definition for a comment on this.
        conv_fact = self.get_conv_fact(mw_grad)
        euler_step_length = self.step_length / (self.max_pred_steps /
                                                conv_fact)

        def taylor_gradient(step):
            """Return gradient from Taylor expansion of energy to 2nd order."""
            return mw_grad + self.mw_hessian @ step

        # These variables will hold the coordinates and gradients along
        # the Euler integration and will be updated frequently.
        euler_mw_coords = self.mw_coords.copy()
        euler_mw_grad = mw_grad.copy()
        self.log(
            f"Predictor-Euler-integration with Δs={euler_step_length:.6f} "
            f"for up to {self.max_pred_steps} steps")
        prev_cur_length = 0.
        for i in range(self.max_pred_steps):
            # Calculate step length in non-mass-weighted coordinates
            cur_length = get_integration_length(euler_mw_coords)
            if i % 50 == 0:
                diff = cur_length - prev_cur_length
                self.log(f"\t{i:03d}: {cur_length:.4f} Δ={diff:.4f}")
                prev_cur_length = cur_length

            # Check if we achieved the desired step length.
            if cur_length >= self.step_length:
                self.log(
                    "Predictor-Euler integration converged with "
                    f"Δs={cur_length:.4f} (desired Δs={self.step_length:.4f}) "
                    f"after {i+1} steps!")
                break
            step_ = euler_step_length * -euler_mw_grad / np.linalg.norm(
                euler_mw_grad)
            euler_mw_coords += step_
            # Determine actual step by comparing the current and the initial coordinates
            euler_step = euler_mw_coords - init_mw_coords
            euler_mw_grad = taylor_gradient(euler_step)
        else:
            self.log(f"Predictor-Euler integration did not converge in {i+1} "
                     f"steps. Δs={cur_length:.4f}.")

            # Check if we are already sufficiently converged. If so signal
            # convergence.
            self.mw_coords = euler_mw_coords

            # Use rms of gradient from taylor expansion for convergence check.
            euler_grad = self.unweight_vec(euler_mw_grad)
            rms_grad = rms(euler_grad)

            # Or check true gradient? But this would need an additional calculation,
            # so I disabled it for now.
            # rms_grad = rms(self.gradient)

            # if rms_grad <= 5*self.rms_grad_thresh:
            if rms_grad <= self.rms_grad_thresh:
                self.log("Sufficient convergence achieved on rms(grad)")
                self.converged = True
                return
        self.log("")

        # Calculate energy and gradient at new predicted geometry. Update the
        # hessian accordingly. These results will be added to the DWI for use
        # in the corrector step.
        self.mw_coords = euler_mw_coords
        self.log("Calculating energy and gradient at predictor step geometry.")
        mw_grad = self.mw_gradient
        energy = self.energy

        # Hessian update
        dx = self.mw_coords - self.irc_mw_coords[-1]
        dg = mw_grad - self.irc_mw_gradients[-1]
        dH, key = self.hessian_update_func(self.mw_hessian, dx, dg)
        self.mw_hessian += dH
        self.log(f"Did {key} hessian update after predictor step.\n")
        self.dwi.update(self.mw_coords.copy(), energy, mw_grad,
                        self.mw_hessian.copy())
        if self.dump_dwi:
            self.dwi.dump(
                f"dwi_{self.cur_direction}_{self.cur_cycle:0{self.cycle_places}d}.h5"
            )

        corrected_mw_coords = self.corr_func(init_mw_coords, self.step_length,
                                             self.dwi)
        self.mw_coords = corrected_mw_coords
        corr_step_length = get_integration_length(self.mw_coords)
        self.log(f"Corrected unweighted step length: {corr_step_length:.6f}")