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.")
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()
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
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
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
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