Exemple #1
0
def test_run_aims():
    from carmm.run.aims_path import set_aims_command

    for hpc in ['hawk', 'isambard', 'archer2', 'young']:
        set_aims_command(hpc)

    import ase  # Necessary to check the code version, as socket functionality has changed
    ase_major_version = int(ase.__version__.split(".")[0])
    ase_minor_version = int(ase.__version__.split(".")[1])

    from ase.calculators.aims import Aims
    from carmm.run.aims_calculator import get_aims_and_sockets_calculator

    for state in range(4):
        #fhi_calc = get_aims_calculator(state)
        sockets_calc, fhi_calc = get_aims_and_sockets_calculator(state,
                                                                 verbose=True)

        # Assertion test that the correct calculators are being set
        # ASE version 3.21 or earlier
        if ase_major_version <= 3 and ase_minor_version <= 21:
            assert (type(sockets_calc.calc) == Aims)
        else:
            # ASE Version 3.22 or later
            assert (type(sockets_calc.launch_client.calc) == Aims)
def pre_neb_aims(initial,
                 final,
                 hpc="hawk",
                 basis_set ='light',
                 filename="last_predicted_path.traj"):


    '''
    This function performs a preliminary NEB calculation on user-provided
    structures using ML-NEB. If the calculation does not converge within
    75 steps, it is terminated. Minimum Energy Path energy landscape is
    examined for occurence of multiple local maxima and if detected - geometry
    optimisations on local minima are performed.

    The optimised structures can be used as alternative start/end points for
    further calculations making the NEB calculation easier to converge.

    Parameters:
    hpc: string
        'hawk', 'isambard', 'archer' see carmm.run.aims_path.set_aims_command
    basis_set: string
        'light', 'tight' etc., see carmm.run.aims_path.set_aims_command
    filename: string
        Name of a file containing an unconverged NEB Minimum Energy Path.
        Default is 'last_predicted_path.traj' for CatLearn MLNEB.
    initial: Atoms object
        Starting geometry of a NEB calculation.
    final: Atoms object
        End geometry of a NEB calculation.
    '''
    import os

    if not os.path.exists(filename):
        from ase.io import read
        from catlearn.optimize.mlneb import MLNEB

        # Set the environment parameters
        from carmm.run.aims_path import set_aims_command
        set_aims_command(hpc=hpc, basis_set=basis_set)

        # your settings go here
        def my_calc():
            # New method that gives a default calculator
            from carmm.run.aims_calculator import get_aims_calculator
            return get_aims_calculator(dimensions=2)

        from carmm.build.neb.ilm import neb_identify_local_minima
        from carmm.build.neb.ilm import multiple_local_extrema

        # Desired number of images including start and end point
        # Enough to show energy landscape of Minimum Energy Path
        n = 15

        calculator = my_calc()

        # Setup the Catlearn object for MLNEB
        neb_catlearn = MLNEB(start=initial,
                             end=final,
                             ase_calc=calculator,
                             n_images=n,
                             interpolation='idpp', restart=False)

        # Run the NEB optimisation. Adjust fmax to desired convergence criteria,
        # usually 0.01 ev/A. Max steps set to 75 for preliminary study.
        # MLNEB serial part is quick below 100 structures

        neb_catlearn.run(fmax=0.01,
                         trajectory='ML-NEB.traj',
                         full_output=False,
                         steps=75)

    if multiple_local_extrema(filename=filename) is True:
        print("Multiple extrema detected in the predicted Minimum Energy Path.")
        print("Local minima will be identified and optimised")
        atoms_list, indices = neb_identify_local_minima(filename=filename)

        print(len(atoms_list), "minima detected. Performing geometry optimisations.")

        from ase.optimize import BFGS
        from carmm.run.aims_path import set_aims_command
        from carmm.run.aims_calculator import get_aims_calculator
        set_aims_command(hpc=hpc, basis_set=basis_set)

        x = 0
        for atoms in atoms_list:
            id = indices[x]
            atoms.calc = get_aims_calculator(2, k_grid=(3, 3, 1))
            opt = BFGS(atoms,
                       restart="min_"+str(id)+".pckl",
                       trajectory="min_"+str(id)+".traj")
            opt.run(fmax=0.01)
            x = x+1

        print("Geometry optimisations completed.")
        print("Please consider the structures as alternative start/end points.")

    else:
        print("No multiple extrema detected in the predicted Minimum Energy Path.")
Exemple #3
0
    def vibrate(self, atoms: Atoms, indices: list, read_only=False):
        '''

        This method uses ase.vibrations module, see more for info.
        User provides the FHI-aims parameters, the Atoms object and list
        of indices of atoms to be vibrated. Variables related to FHI-aims are governed by the React object.
        Calculation folders are generated automatically and a sockets calculator is used for efficiency.

        Work in progress

        Args:
            atoms: Atoms object
            indices: list
                List of indices of atoms that require vibrations
            read_only: bool
                Flag for postprocessing - if True, the method only extracts information from existing files,
                no calculations are performed

        Returns:
            Zero-Point Energy: float
        '''
        '''Retrieve common properties'''
        basis_set = self.basis_set
        hpc = self.hpc
        params = self.params
        parent_dir = os.getcwd()
        dimensions = sum(atoms.pbc)

        if not self.filename:
            '''develop a naming scheme based on chemical formula'''
            self.filename = atoms.get_chemical_formula()

        vib_dir = parent_dir + "/VibData_" + self.filename + "/Vibs"
        print(vib_dir)

        vib = Vibrations(atoms, indices=indices, name=vib_dir)
        '''If a calculation was terminated prematurely (e.g. time limit) empty .json files remain and the calculation
        of the corresponding stretch modes would be skipped on restart. The line below prevents this'''
        vib.clean(empty_files=True)
        '''Extract vibration data from existing files'''
        if read_only:
            vib.read()

        else:
            '''Calculate required vibration modes'''
            required_cache = [
                os.path.join(vib_dir, "cache." + str(x) + y + ".json")
                for x in indices
                for y in ["x+", "x-", "y+", "y-", "y-", "z+", "z-"]
            ]
            check_required_modes_files = np.array(
                [os.path.exists(file) for file in required_cache])

            if np.all(check_required_modes_files == True):
                vib.read()
            else:
                '''Set the environment variables for geometry optimisation'''
                set_aims_command(hpc=hpc,
                                 basis_set=basis_set,
                                 defaults=2020,
                                 nodes_per_instance=self.nodes_per_instance)
                '''Generate a unique folder for aims calculation'''
                counter, subdirectory_name = self._restart_setup(
                    "Vib",
                    filename=self.filename,
                    restart=False,
                    verbose=False)

                os.makedirs(subdirectory_name, exist_ok=True)
                os.chdir(subdirectory_name)
                '''Name the aims output file'''
                out = str(counter) + "_" + str(self.filename) + ".out"
                '''Calculate vibrations and write the in a separate directory'''
                with _calc_generator(params, out_fn=out,
                                     dimensions=dimensions)[0] as calculator:
                    if not self.dry_run:
                        atoms.calc = calculator
                    else:
                        atoms.calc = EMT()

                    vib = Vibrations(atoms, indices=indices, name=vib_dir)
                    vib.run()

            vib.summary()
        '''Generate a unique folder for aims calculation'''
        if not read_only:
            os.chdir(vib_dir)
            vib.write_mode()
        os.chdir(parent_dir)

        return vib.get_zero_point_energy()
Exemple #4
0
    def aims_optimise(self,
                      atoms: Atoms,
                      fmax: float = 0.01,
                      post_process: str = None,
                      relax_unit_cell: bool = False,
                      restart: bool = True,
                      verbose: bool = True):
        '''
        The function needs information about structure geometry (model), name of hpc system
        to configure FHI-aims environment variables (hpc). Separate directory is created with a naming convention
        based on chemical formula and number of restarts, n (opt_formula_n), ensuring that no outputs are overwritten
        in ASE/FHI-aims.
        The geometry optimisation is restarted from a new Hessian each 80 steps in BFGS algorithm to overcome deep
        potential energy local minima with fmax above convergence criteria. One can choose the type of phase of
        the calculation (gas, surface, bulk) and request a post_processing calculation with a larger basis set.

        PARAMETERS:
        params: dict
            Dictionary containing user's calculator FHI-aims parameters
        atoms: Atoms object
            Contains the geometry information for optimisation
        fmax: float
            Force convergence criterion for geometry optimisation, i.e. max forces on any atom in eV/A
        post_process: str or None
            Basis set to be used for post_processing if energy calculation using a larger basis set is required
        relax_unit_cell: bool
            True requests a strain filter unit cell relaxation
        restart: bool
            Request restart from previous geometry if True (True by default)

        Returns a list containing the model with data calculated using
        light and tight settings: [model_light, model_tight]
        '''
        from ase.io import read
        from ase.io.trajectory import Trajectory
        from carmm.analyse.forces import is_converged
        from ase.optimize import BFGS
        '''Setup initial parameters'''
        params = self.params
        hpc = self.hpc
        basis_set = self.basis_set
        self.initial = atoms
        dimensions = sum(self.initial.pbc)
        i_geo = atoms.copy()
        i_geo.calc = atoms.calc
        '''parent directory'''
        parent_dir = os.getcwd()
        '''Read the geometry'''
        if not self.filename:
            self.filename = self.initial.get_chemical_formula()

        filename = self.filename

        counter, subdirectory_name = self._restart_setup("Opt",
                                                         self.filename,
                                                         restart,
                                                         verbose=verbose)
        out = str(counter) + "_" + str(filename) + ".out"
        '''Perform calculation only if required'''
        if is_converged(atoms, fmax):
            if verbose:
                print("The forces are below", fmax,
                      "eV/A. No calculation required.")
            self.model_optimised = self.atoms
        elif is_converged(self.initial, fmax):
            if verbose:
                print("The forces are below", fmax,
                      "eV/A. No calculation required.")
            self.model_optimised = self.initial
            self.initial = i_geo
        else:
            os.makedirs(subdirectory_name, exist_ok=True)
            os.chdir(subdirectory_name)
            '''Set the environment variables for geometry optimisation'''
            set_aims_command(hpc=hpc,
                             basis_set=basis_set,
                             defaults=2020,
                             nodes_per_instance=self.nodes_per_instance)
            '''Occasional optimizer restarts will prevent the calculation from getting stuck in deep local minimum'''
            opt_restarts = 0
            '''Perform DFT calculations for each filename'''
            with _calc_generator(
                    params,
                    out_fn=out,
                    dimensions=dimensions,
                    relax_unit_cell=relax_unit_cell)[0] as calculator:

                if not self.dry_run:
                    self.initial.calc = calculator
                else:
                    self.initial.calc = EMT()

                while not is_converged(self.initial, fmax):
                    if relax_unit_cell:
                        from ase.constraints import StrainFilter
                        unit_cell_relaxer = StrainFilter(self.initial)

                        opt = BFGS(unit_cell_relaxer,
                                   trajectory=str(counter) + "_" + filename +
                                   "_" + str(opt_restarts) + ".traj",
                                   alpha=70.0)
                    else:
                        opt = BFGS(self.initial,
                                   trajectory=str(counter) + "_" + filename +
                                   "_" + str(opt_restarts) + ".traj",
                                   alpha=70.0)

                    opt.run(fmax=fmax, steps=80)
                    opt_restarts += 1

            self.model_optimised = read(
                str(counter) + "_" + filename + "_" + str(opt_restarts - 1) +
                ".traj")
            os.chdir(parent_dir)

        self.initial = i_geo

        if post_process:
            if verbose:
                print("Commencing calculation using", post_process,
                      "basis set.")

            model_pp = self.model_optimised.copy()
            '''Set environment variables for a larger basis set - converged electronic structure'''
            subdirectory_name_tight = subdirectory_name + "_" + post_process
            os.makedirs(subdirectory_name_tight, exist_ok=True)
            os.chdir(subdirectory_name_tight)

            set_aims_command(hpc=hpc,
                             basis_set=post_process,
                             defaults=2020,
                             nodes_per_instance=self.nodes_per_instance)
            '''Recalculate the structure using a larger basis set in a separate folder'''
            with _calc_generator(params,
                                 out_fn=str(self.filename) + "_" +
                                 post_process + ".out",
                                 forces=False,
                                 dimensions=dimensions)[0] as calculator:
                if not self.dry_run:
                    model_pp.calc = calculator
                else:
                    model_pp.calc = EMT()

                model_pp.get_potential_energy()
                traj = Trajectory(self.filename + "_" + post_process + ".traj",
                                  "w")
                traj.write(model_pp)
                traj.close()
            '''Go back to the parent directory to finish the loop'''
            os.chdir(parent_dir)
            '''update the instance with a post_processed model'''
            self.model_post_processed = model_pp

        return self.model_optimised, self.model_post_processed
Exemple #5
0
    def search_ts_taskfarm(self,
                           initial,
                           final,
                           fmax,
                           n,
                           method="string",
                           interpolation="idpp",
                           input_check=0.01,
                           max_steps=100,
                           verbose=True):
        '''

        Args:
            initial: Atoms object
                Initial structure in the NEB band
            final: Atoms object
                Final structure in the NEB band
            fmax: float
                Convergence criterion of forces in eV/A
            n: int
                number of middle images, the following is recommended: n * npi = total_no_CPUs
            interpolation: str or []
                The "idpp" or "linear" interpolation types are supported in ASE. alternatively user can provide a custom
                interpolation as a list of Atoms objects.
            input_check: float or None
                If float the calculators of the input structures will be checked if the structures are below
                the requested fmax and an optimisation will be performed if not.
            max_steps: int
                Maximum number of iteration before stopping the optimizer
            verbose: bool
                Flag for turning off printouts in the code

        Returns: Atoms object
            Transition state geometry structure

        '''
        from ase.neb import NEB
        from ase.optimize import FIRE
        '''Retrieve common properties'''
        basis_set = self.basis_set
        hpc = self.hpc
        dimensions = sum(initial.pbc)
        params = self.params
        parent_dir = os.getcwd()
        '''Set the environment parameters'''
        set_aims_command(hpc=hpc,
                         basis_set=basis_set,
                         defaults=2020,
                         nodes_per_instance=self.nodes_per_instance)
        '''Read the geometry'''
        if self.filename:
            filename = self.filename
        else:
            filename = initial.get_chemical_formula()

        counter, subdirectory_name = self._restart_setup("TS",
                                                         filename,
                                                         restart=False,
                                                         verbose=verbose)
        '''Ensure input is converged'''
        if input_check:
            npi = self.nodes_per_instance
            self.nodes_per_instance = None

            if not is_converged(initial, input_check):
                self.filename = filename + "_initial"
                initial = self.aims_optimise(initial,
                                             input_check,
                                             restart=False,
                                             verbose=False)[0]
            if not is_converged(final, input_check):
                self.filename = filename + "_final"
                final = self.aims_optimise(final,
                                           input_check,
                                           restart=False,
                                           verbose=False)[0]
            '''Set original name after input check is complete'''
            self.nodes_per_instance = npi
            self.filename = filename

        out = str(counter) + "_" + str(filename) + ".out"

        os.makedirs(subdirectory_name, exist_ok=True)
        os.chdir(subdirectory_name)

        if interpolation in ["idpp", "linear"]:
            images = [initial]
            for i in range(n):
                image = initial.copy()
                if not self.dry_run:
                    image.calc = _calc_generator(params,
                                                 out_fn=str(i) + "_" + out,
                                                 dimensions=dimensions)[0]
                    image.calc.launch_client.calc.directory = "./" + str(
                        i) + "_" + out[:-4]
                else:
                    image.calc = EMT()
                    image.calc.directory = "./" + str(i) + "_" + out[:-4]

                images.append(image)

            images.append(final)
        elif isinstance(interpolation, list):
            assert [
                isinstance(i, Atoms) for i in interpolation
            ], "Interpolation must be a list of Atoms objects, 'idpp' or 'linear'!"
            assert len(
                interpolation
            ) - 2 == n, "Number of middle images is fed interpolation must match specified n to ensure correct parallelisation"

            images = interpolation
            for i in range(1, len(interpolation) - 1):
                # use i-1 for name to retain folder naming as per "idpp"
                if not self.dry_run:
                    images[i].calc = _calc_generator(params,
                                                     out_fn=str(i - 1) + "_" +
                                                     out,
                                                     dimensions=dimensions)[0]
                else:
                    images[i].calc = EMT()
                images[i].calc.launch_client.calc.directory = "./" + str(
                    i - 1) + "_" + out[:-4]
        else:
            raise ValueError(
                "Interpolation must be a list of Atoms objects, 'idpp' or 'linear'!"
            )

        neb = NEB(images,
                  k=0.05,
                  method=method,
                  climb=True,
                  parallel=True,
                  allow_shared_calculator=False)
        if interpolation in ["idpp", "linear"]:
            neb.interpolate(method=interpolation,
                            mic=True,
                            apply_constraint=True)

        qn = FIRE(neb, trajectory='neb.traj')
        qn.run(fmax=fmax, steps=max_steps)

        for image in images[1:-1]:
            if not self.dry_run:
                image.calc.close()
        '''Find maximum energy, i.e. transition state to return it'''
        self.ts = sorted(images,
                         key=lambda k: k.get_potential_energy(),
                         reverse=True)[0]
        os.chdir(parent_dir)

        return self.ts
Exemple #6
0
    def search_ts_aidneb(self,
                         initial,
                         final,
                         fmax,
                         unc,
                         interpolation="idpp",
                         n=15,
                         restart=True,
                         prev_calcs=None,
                         input_check=0.01,
                         verbose=True):
        '''
        This function allows calculation of the transition state using the GPAtom software package in an
        ASE/sockets/FHI-aims setup. The resulting converged band will be located in the AIDNEB.traj file.

        Args:
            initial: Atoms object
                Initial structure in the NEB band
            final: Atoms object
                Final structure in the NEB band
            fmax: float
                Convergence criterion of forces in eV/A
            unc: float
                Uncertainty in the fit of the NEB according to the Gaussian Progress Regression model, a secondary
                convergence criterion.
            n: int
                number of middle images, the following is recommended: n * npi = total_no_CPUs
            interpolation: str or []
                The "idpp" or "linear" interpolation types are supported in ASE. alternatively user can provide a custom
                interpolation as a list of Atoms objects.
            n: int or flot
                Desired number of middle images excluding start and end point. If float the number of images is based on
                displacement of atoms. Dense sampling aids convergence but does not increase complexity as significantly
                as for classic NEB.
            restart: bool
                Use previous calculations contained in folders if True, start from scratch if False
            prev_calcs: list of Atoms objects
                Manually provide the training set
           input_check: float or None
                If float the calculators of the input structures will be checked if the structures are below
                the requested fmax and an optimisation will be performed if not.
            verbose: bool
                Flag for turning off printouts in the code

        Returns: Atoms object
            Transition state geometry structure

        '''
        from gpatom.aidneb import AIDNEB
        '''Retrieve common properties'''
        basis_set = self.basis_set
        hpc = self.hpc
        dimensions = sum(initial.pbc)
        params = self.params
        parent_dir = os.getcwd()
        '''Set the environment parameters'''
        set_aims_command(hpc=hpc,
                         basis_set=basis_set,
                         defaults=2020,
                         nodes_per_instance=self.nodes_per_instance)

        if not interpolation:
            interpolation = "idpp"
        '''Read the geometry'''
        if self.filename:
            filename = self.filename
        else:
            filename = initial.get_chemical_formula()
            self.filename = filename
        '''Check for previous calculations'''
        counter, subdirectory_name = self._restart_setup("TS",
                                                         filename,
                                                         restart=restart,
                                                         verbose=verbose)
        '''Let the user restart from alternative file or Atoms object'''
        if prev_calcs:
            self.prev_calcs = prev_calcs
            if verbose:
                print(
                    "User provided a list of structures manually, training set substituted."
                )

        if os.path.exists(
                os.path.join(subdirectory_name[:-1] + str(counter - 1),
                             "AIDNEB.traj")):
            previously_converged_ts_search = os.path.join(
                subdirectory_name[:-1] + str(counter - 1), "AIDNEB.traj")
            if verbose:
                print("TS search already converged at",
                      previously_converged_ts_search)

            neb = read(previously_converged_ts_search + "@:")
            self.ts = sorted(neb,
                             key=lambda k: k.get_potential_energy(),
                             reverse=True)[0]
            os.chdir(parent_dir)

            return self.ts

        elif input_check:
            if not is_converged(initial, input_check):
                self.filename += "_initial"
                initial = self.aims_optimise(initial,
                                             input_check,
                                             restart=False,
                                             verbose=verbose)[0]
                self.initial = self.model_optimised
                '''Set original name after input check is complete'''
                self.filename = filename

            if not is_converged(final, input_check):
                self.filename = filename + "_final"
                final = self.aims_optimise(final,
                                           input_check,
                                           restart=False,
                                           verbose=verbose)[0]
                self.final = self.model_optimised
                '''Set original name after input check is complete'''
                self.filename = filename

        out = str(counter) + "_" + str(filename) + ".out"

        os.makedirs(subdirectory_name, exist_ok=True)
        os.chdir(subdirectory_name)

        # TODO: calculating initial and final structure if possible within the GPAtom code
        '''sockets setup'''
        with _calc_generator(params, out_fn=out,
                             dimensions=dimensions)[0] as calculator:

            if self.dry_run:
                calculator = EMT()
            '''Setup the GPAtom object for AIDNEB'''
            aidneb = AIDNEB(
                start=initial,
                end=final,
                interpolation=interpolation,
                # "idpp" can in some cases (e.g. H2) result in geometry coordinates returned as NaN, no error exit, but calculator stuck
                calculator=calculator,
                n_images=n + 2,
                max_train_data=50,
                trainingset=self.prev_calcs,
                use_previous_observations=True,
                neb_method='improvedtangent',
                mic=True)
            '''Run the NEB optimisation. Adjust fmax to desired convergence criteria, usually 0.01 ev/A'''
            if not self.dry_run:
                aidneb.run(fmax=fmax, unc_convergence=unc, ml_steps=100)
            else:
                os.chdir(parent_dir)
                return None
        '''Find maximum energy, i.e. transition state to return it'''
        neb = read("AIDNEB.traj@:")
        self.ts = sorted(neb,
                         key=lambda k: k.get_potential_energy(),
                         reverse=True)[0]
        os.chdir(parent_dir)

        return self.ts
Exemple #7
0
    def get_mulliken_charges(self, initial: Atoms, verbose=True):
        '''
        This function is used to retrieve atomic charges using Mulliken charge
        decomposition as implemented in FHI-aims. A new trajectory file containing
        the charges

        Args:
            initial: Atoms
                Atoms object containing structural information for the calculation
            verbose: bool
                Flag for turning off printouts in the code

        Returns:
            Atoms object with charges appended
        '''

        from ase.io.trajectory import Trajectory
        from carmm.analyse.mulliken import extract_mulliken_charge
        '''Setup initial parameters'''
        params = self.params
        hpc = self.hpc
        basis_set = self.basis_set
        self.initial = initial
        dimensions = sum(self.initial.pbc)
        '''Parent directory'''
        parent_dir = os.getcwd()
        '''Read the geometry'''
        if not self.filename:
            self.filename = self.initial.get_chemical_formula()

        filename = self.filename
        assert type(filename) == str, "Invalid type, filename should be string"

        counter, subdirectory_name = self._restart_setup(
            "Charges", self.filename)
        '''Check for previously completed calculation'''
        if os.path.exists(
                os.path.join(subdirectory_name[:-1] + str(counter - 1),
                             filename + "_charges.traj")):
            file_location = os.path.join(
                subdirectory_name[:-1] + str(counter - 1),
                filename + "_charges.traj")
            self.initial = read(file_location)
            if verbose:
                print("Previously calculated structure has been found at",
                      file_location)
            return self.initial

        out = str(counter) + "_" + str(filename) + ".out"
        '''Set the environment variables for geometry optimisation'''
        set_aims_command(hpc=hpc, basis_set=basis_set, defaults=2020)
        '''Request Mulliken charge decomposition'''
        params["output"] = ["Mulliken_summary"]

        os.makedirs(subdirectory_name, exist_ok=True)
        os.chdir(subdirectory_name)

        with _calc_generator(params,
                             out_fn=out,
                             dimensions=dimensions,
                             forces=False)[0] as calculator:
            if not self.dry_run:
                self.initial.calc = calculator
            else:
                self.initial.calc = EMT()

            self.initial.get_potential_energy()

        if not self.dry_run:
            charges = extract_mulliken_charge(out, len(self.initial))
        else:
            charges = initial.get_charges()

        self.initial.set_initial_charges(charges)

        traj = Trajectory(filename + "_charges.traj", 'w')
        traj.write(self.initial)
        traj.close()

        os.chdir(parent_dir)

        return self.initial