def check_if_atoms_interacting_force(model, symbols, ftol): """ Construct a dimer and try to decrease its bond length until the force acting on each atom is larger than 'ftol' in magnitude. The 'symbols' arg should be a list or tuple of length 2 indicating which species pair to check, e.g. to check if Al interacts with Al, one should specify ['Al', 'Al']. """ if not isinstance(symbols, (list, tuple)) or len(symbols) != 2: raise ValueError( "Argument 'symbols' passed to check_if_atoms_interacting_force " "must be a list of tuple of length 2 indicating the species pair to " "check" ) dimer = Atoms( symbols, positions=[(0.1, 0.1, 0.1), (5.1, 0.1, 0.1)], cell=(20, 20, 20), pbc=(False, False, False), ) calc = KIM(model) dimer.set_calculator(calc) try: rescale_to_get_nonzero_forces(dimer, ftol) atoms_interacting = True return atoms_interacting except: # noqa: E722 atoms_interacting = False return atoms_interacting finally: if hasattr(calc, "__del__"): calc.__del__() del dimer
def get_isolated_energy_per_atom(model, symbol): """ Construct a non-periodic cell containing a single atom and compute its energy. """ single_atom = Atoms( symbol, positions=[(0.1, 0.1, 0.1)], cell=(20, 20, 20), pbc=(False, False, False), ) calc = KIM(model) single_atom.set_calculator(calc) energy_per_atom = single_atom.get_potential_energy() if hasattr(calc, "__del__"): calc.__del__() del single_atom return energy_per_atom
def check_if_atoms_interacting_energy(model, symbols, etol): """ First, get the energy of a single isolated atom of each species given in 'symbols'. Then, construct a dimer consisting of these two species and try to decrease its bond length until a discernible difference in the energy (from the sum of the isolated energy of each species) is detected. The 'symbols' arg should be a list or tuple of length 2 indicating which species pair to check, e.g. to check if Al interacts with Al, one should specify ['Al', 'Al']. """ if not isinstance(symbols, (list, tuple)) or len(symbols) != 2: raise ValueError( "Argument 'symbols' passed to check_if_atoms_interacting_energy " "must be a list of tuple of length 2 indicating the species pair to " "check" ) isolated_energy_per_atom = {} isolated_energy_per_atom[symbols[0]] = get_isolated_energy_per_atom( model, symbols[0] ) isolated_energy_per_atom[symbols[1]] = get_isolated_energy_per_atom( model, symbols[1] ) dimer = Atoms( symbols, positions=[(0.1, 0.1, 0.1), (5.1, 0.1, 0.1)], cell=(20, 20, 20), pbc=(False, False, False), ) calc = KIM(model) dimer.set_calculator(calc) try: rescale_to_get_nonzero_energy(dimer, isolated_energy_per_atom, etol) atoms_interacting = True return atoms_interacting except: # noqa: E722 atoms_interacting = False return atoms_interacting finally: if hasattr(calc, "__del__"): calc.__del__() del dimer
from ase.lattice.cubic import FaceCenteredCubic from ase.calculators.kim.kim import KIM atoms = FaceCenteredCubic(symbol='Ar', latticeconstant=5.25, size=(1, 1, 1)) calc = KIM('LJ_ElliottAkerson_2015_Universal__MO_959249795837_003') atoms.calc = calc energy = atoms.get_potential_energy() print('Potential energy: {} eV'.format(energy))
#!/usr/bin/env python3 from ase import Atom, Atoms from ase.build import bulk, fcc100, add_adsorbate, add_vacuum from ase.calculators.vasp import Vasp from ase.calculators.kim.kim import KIM from ase.calculators.qmmm import ForceQMMM, RescaledCalculator from ase.constraints import StrainFilter from ase.optimize import LBFGS from ase.visualize import view atoms = bulk("Pd", "fcc", a=3.5, cubic=True) atoms.calc = KIM("MEAM_LAMMPS_JeongParkDo_2018_PdMo__MO_356501945107_000") opt = LBFGS(StrainFilter(atoms), logfile=None) opt.run(0.03, steps=30) length = atoms.cell.cellpar()[0] atoms = fcc100("Pd", (2,2,5), a=length, vacuum=10, periodic=True) add_adsorbate(atoms, Atoms([Atom("Mo")]), 1.2) qm_mask = [len(atoms)-1, len(atoms)-2] qm_calc = Vasp(directory="./qmmm") mm_calc = KIM("MEAM_LAMMPS_JeongParkDo_2018_PdMo__MO_356501945107_000") mm_calc = RescaledCalculator(mm_calc, 1, 1, 1, 1) qmmm = ForceQMMM(atoms, qm_mask, qm_calc, mm_calc, buffer_width=3) qmmm.initialize_qm_buffer_mask(atoms) atoms.pbc=True atoms.calc = qmmm print(atoms.get_forces())
def get_model_energy_cutoff( model, symbols, xtol=1e-8, etol_coarse=1e-6, etol_fine=1e-15, max_bisect_iters=1000, max_upper_cutoff_bracket=20.0, ): """ Compute the distance at which energy interactions become non-trival for a given model and a species pair it supports. This is done by constructing a dimer composed of these species in a large finite box, increasing the separation if necessary until the total potential energy is within 'etol_fine' of the sum of the corresponding isolated energies, and then shrinking the separation until the energy differs from that value by more than 'etol_coarse'. Using these two separations to bound the search range, bisection is used to refine in order to locate the cutoff. The 'symbols' arg should be a list or tuple of length 2 indicating which species pair to check, e.g. to get the energy cutoff of Al with Al, one should specify ['Al', 'Al']. This function is based on the content of the DimerContinuityC1__VC_303890932454_002 Verification Check in OpenKIM [1-3]. [1] Tadmor E. Verification Check of Dimer C1 Continuity v002. OpenKIM; 2018. doi:10.25950/43d2c6d5 [2] Tadmor EB, Elliott RS, Sethna JP, Miller RE, Becker CA. The potential of atomistic simulations and the Knowledgebase of Interatomic Models. JOM. 2011;63(7):17. doi:10.1007/s11837-011-0102-6 [3] Elliott RS, Tadmor EB. Knowledgebase of Interatomic Models (KIM) Application Programming Interface (API). OpenKIM; 2011. doi:10.25950/ff8f563a """ from scipy.optimize import bisect def get_dimer_positions(a, large_cell_len): """ Generate positions for a dimer of length 'a' centered in a finite simulation box with side length 'large_cell_len' """ half_cell = 0.5 * large_cell_len positions = [ [half_cell - 0.5 * a, half_cell, half_cell], [half_cell + 0.5 * a, half_cell, half_cell], ] return positions def energy(a, dimer, large_cell_len, einf): dimer.set_positions(get_dimer_positions(a, large_cell_len)) return dimer.get_potential_energy() - einf def energy_cheat(a, dimer, large_cell_len, offset, einf): dimer.set_positions(get_dimer_positions(a, large_cell_len)) return (dimer.get_potential_energy() - einf) + offset if not isinstance(symbols, (list, tuple)) or len(symbols) != 2: raise ValueError( "Argument 'symbols' passed to check_if_atoms_interacting_energy " "must be a list of tuple of length 2 indicating the species pair to " "check" ) isolated_energy_per_atom = {} isolated_energy_per_atom[symbols[0]] = get_isolated_energy_per_atom( model, symbols[0] ) isolated_energy_per_atom[symbols[1]] = get_isolated_energy_per_atom( model, symbols[1] ) einf = isolated_energy_per_atom[symbols[0]] + isolated_energy_per_atom[symbols[1]] # First, establish the upper bracket cutoff by starting at 'b_init' Angstroms and # incrementing by 'db' until b_init = 4.0 # Create finite box of size large_cell_len large_cell_len = 50 dimer = Atoms( symbols, positions=get_dimer_positions(b_init, large_cell_len), cell=(large_cell_len, large_cell_len, large_cell_len), pbc=(False, False, False), ) calc = KIM(model) dimer.set_calculator(calc) db = 2.0 b = b_init - db still_interacting = True while still_interacting: b += db if b > max_upper_cutoff_bracket: if hasattr(calc, "__del__"): calc.__del__() raise KIMASEError( "Exceeded limit on upper bracket when determining cutoff " "search range" ) else: eb = energy(b, dimer, large_cell_len, einf) if abs(eb) < etol_fine: still_interacting = False a = b da = 0.01 not_interacting = True while not_interacting: a -= da if a < 0: if hasattr(calc, "__del__"): calc.__del__() raise RuntimeError( "Failed to determine lower bracket for cutoff search using etol_coarse " "= {}. This may mean that the species pair provided ({}) does not " "have a non-trivial energy interaction for the potential being " "used.".format(etol_coarse, symbols) ) else: ea = energy(a, dimer, large_cell_len, einf) if abs(ea) > etol_coarse: not_interacting = False # NOTE: Some Simulator Models have a history dependence due to them maintaining # charges from the previous energy evaluation to use as an initial guess # for the next charge equilibration. We therefore have to treat them not # as single-valued functions but as distributions, i.e. for a given # configuration you might get any of a range of energy values depending on # the history of your previous energy evaluations. This is particularly # problematic for this step, where we set up a bisection problem in order # to determine the cutoff radius of the model. Our solution for this # specific case is to make a very crude estimate of the variance of that # distribution with a 10% factor of safety on it. eb_new = energy(b, dimer, large_cell_len, einf) eb_error = abs(eb_new - eb) # compute offset to ensure that energy before and after cutoff have # different signs if ea < eb: offset = -eb + 1.1 * eb_error + np.finfo(float).eps else: offset = -eb - 1.1 * eb_error - np.finfo(float).eps rcut, results = bisect( energy_cheat, a, b, args=(dimer, large_cell_len, offset, einf), full_output=True, xtol=xtol, maxiter=max_bisect_iters, ) # General clean-up if hasattr(calc, "__del__"): calc.__del__() if not results.converged: raise RuntimeError( "Bisection search to find cutoff distance did not converge " "within {} iterations with xtol = {}".format(max_bisect_iters, xtol) ) else: return rcut
def run_md(atoms, id, settings_fn): """The function does Molecular Dyanamic simulation (MD) on a material, given by argument atoms. Parameters: atoms (obj): an atoms object defined by class in ase. This is the material which MD will run on. id (int): an identifying number for the material. Returns: obj:atoms object defined in ase, is returned. """ # Read settings settings = read_settings_file(settings_fn) initial_unitcell_atoms = copy.deepcopy(atoms) # Scale atoms object, cubic size = settings['supercell_size'] atoms = atoms * size * (1, 1, 1) # atoms = atoms * (size,size,size) #print(atoms.get_chemical_symbols()) N = len(atoms.get_chemical_symbols()) # Use KIM for potentials from OpenKIM use_kim = settings['use_kim'] # Use Asap for a huge performance increase if it is installed use_asap = True # Create a copy of the initial atoms object for future reference old_atoms = copy.deepcopy(atoms) # Describe the interatomic interactions with OpenKIM potential if use_kim: # use KIM potential atoms.calc = KIM( "LJ_ElliottAkerson_2015_Universal__MO_959249795837_003") else: # otherwise, default to asap3 LennardJones atoms.calc = LennardJones([18], [0.010323], [3.40], rCut=6.625, modified=True) # Set the momenta corresponding to temperature from settings file MaxwellBoltzmannDistribution(atoms, settings['temperature'] * units.kB) # Select integrator if settings['ensemble'] == "NVE": from ase.md.verlet import VelocityVerlet dyn = VelocityVerlet(atoms, settings['time_step'] * units.fs) elif settings['ensemble'] == "NVT": from ase.md.langevin import Langevin dyn = Langevin(atoms, settings['time_step'] * units.fs, settings['temperature'] * units.kB, settings['friction']) interval = settings['interval'] # Creates trajectory files in directory trajectory_files traj = Trajectory("trajectory_files/" + id + ".traj", 'w', atoms) dyn.attach(traj.write, interval=interval) # Number of decimals for most calculated properties. decimals = settings['decimals'] # Boolean indicating if the material is monoatomic. monoatomic = len(set(atoms.get_chemical_symbols())) == 1 # Calculation and writing of properties properties.initialize_properties_file(atoms, initial_unitcell_atoms, id, decimals, monoatomic) dyn.attach(properties.calc_properties, 100, old_atoms, atoms, id, decimals, monoatomic) # unnecessary, used for logging md runs # we should write some kind of logger for the MD def logger(a=atoms): # store a reference to atoms in the definition. """Function to print the potential, kinetic and total energy.""" epot = a.get_potential_energy() / len(a) ekin = a.get_kinetic_energy() / len(a) t = ekin / (1.5 * units.kB) print('Energy per atom: Epot = %.3feV Ekin = %.3feV (T=%3.0fK) ' 'Etot = %.3feV' % (epot, ekin, t, epot + ekin)) # Running the dynamics dyn.attach(logger, interval=interval) #logger() #dyn.run(settings['max_steps']) # check for thermal equilibrium counter = 0 equilibrium = False for i in range(round(settings['max_steps'] / settings['search_interval'])): # hyperparameter epot, ekin_pre, etot, t = properties.energies_and_temp(atoms) # kör steg som motsvarar säg 5 fs dyn.run(settings['search_interval']) # hyperparamter epot, ekin_post, etot, t = properties.energies_and_temp(atoms) #print(abs(ekin_pre-ekin_post) / math.sqrt(N)) #print(counter) if (abs(ekin_pre - ekin_post) / math.sqrt(N)) < settings['tolerance']: counter += 1 else: counter = 0 if counter > settings['threshold']: # hyperparameter print("reached equilibrium") equilibrium = True break if equilibrium: dyn.run(settings['max_steps']) properties.finalize_properties_file(atoms, id, decimals, monoatomic) else: properties.delete_properties_file(id) raise RuntimeError("MD did not find equilibrium") return atoms
def run_md(atoms, id): # Read settings settings = read_settings_file() # Use KIM for potentials from OpenKIM use_kim = True # Use Asap for a huge performance increase if it is installed use_asap = True # Create a copy of the initial atoms object for future reference old_atoms = copy.deepcopy(atoms) # Describe the interatomic interactions with OpenKIM potential if use_kim: # use KIM potential atoms.calc = KIM( "LJ_ElliottAkerson_2015_Universal__MO_959249795837_003") else: # otherwise, default to asap3 LennardJones atoms.calc = LennardJones([18], [0.010323], [3.40], rCut=6.625, modified=True) # Set the momenta corresponding to temperature from settings file MaxwellBoltzmannDistribution(atoms, settings['temperature'] * units.kB) # Select integrator if settings['ensemble'] == "NVE": from ase.md.verlet import VelocityVerlet dyn = VelocityVerlet(atoms, settings['time_step'] * units.fs) elif settings['ensemble'] == "NVT": from ase.md.langevin import Langevin dyn = Langevin(atoms, settings['time_step'] * units.fs, settings['temperature'] * units.kB, settings['friction']) traj = Trajectory('ar.traj', 'w', atoms) dyn.attach(traj.write, interval=1000) # Identity number given as func. parameter to keep track of properties # Number of decimals for most calculated properties decimals = settings['decimals'] # Calculation and writing of properties properties.initialize_properties_file(atoms, id, decimals) dyn.attach(properties.calc_properties, 100, old_atoms, atoms, id, decimals) # unnecessary, used for logging md runs # we should write some kind of logger for the MD def logger(a=atoms): # store a reference to atoms in the definition. """Function to print the potential, kinetic and total energy.""" epot = a.get_potential_energy() / len(a) ekin = a.get_kinetic_energy() / len(a) t = ekin / (1.5 * units.kB) print('Energy per atom: Epot = %.3feV Ekin = %.3feV (T=%3.0fK) ' 'Etot = %.3feV' % (epot, ekin, t, epot + ekin)) # Running the dynamics dyn.attach(logger, interval=1000) logger() dyn.run(settings['max_steps']) return atoms