def run_autode(configuration, max_force=None, method=None, n_cores=1): """ Run an orca or xtb calculation -------------------------------------------------------------------------- :param configuration: (gaptrain.configurations.Configuration) :param max_force: (float) or None :param method: (autode.wrappers.base.ElectronicStructureMethod) """ from autode.species import Species from autode.calculation import Calculation from autode.exceptions import CouldNotGetProperty if method.name == 'orca' and GTConfig.orca_keywords is None: raise ValueError("For ORCA training GTConfig.orca_keywords must be" " set. e.g. " "GradientKeywords(['PBE', 'def2-SVP', 'EnGrad'])") # optimisation is not implemented, needs a method to run assert max_force is None and method is not None species = Species(name=configuration.name, atoms=configuration.atoms, charge=configuration.charge, mult=configuration.mult) # allow for an ORCA calculation to have non-default keywords.. not the # cleanest implementation.. kwds = GTConfig.orca_keywords if method.name == 'orca' else method.keywords.grad calc = Calculation(name='tmp', molecule=species, method=method, keywords=kwds, n_cores=n_cores) calc.run() ha_to_ev = 27.2114 try: configuration.forces = -ha_to_ev * calc.get_gradients() except CouldNotGetProperty: logger.error('Failed to set forces') configuration.energy = ha_to_ev * calc.get_energy() configuration.partial_charges = calc.get_atomic_charges() return configuration
def run_gapmd(configuration, gap, temp, dt, interval, init_temp=None, **kwargs): """ Run molecular dynamics on a system using a GAP to predict energies and forces --------------------------------------------------------------------------- :param configuration: (gaptrain.configurations.Configuration) :param gap: (gaptrain.gap.GAP | gaptrain.gap.AdditiveGAP) :param temp: (float) Temperature in K to initialise velocities and to run NVT MD, if temp=0 then will run NVE :param init_temp: (float | None) Initial temperature to initialise momenta with. If None then will be set at temp :param dt: (float) Timestep in fs :param interval: (int) Interval between printing the geometry ------------------------------------------------- Keyword Arguments: {fs, ps, ns}: Simulation time in some units bbond_energy: (dict | None) Additional energy to add to a breaking bond. e.g. bbond_energy={(0, 1), 0.1} Adds 0.1 eV to the 'bond' between atoms 0 and 1 as velocities shared between the atoms in the breaking bond direction :fbond_energy: (dict | None) As bbond_energy but in the direction to form a bond :returns: (gt.Trajectory) """ logger.info('Running GAP MD') configuration.save(filename='config.xyz') a, b, c = configuration.box.size n_steps = simulation_steps(dt, kwargs) if 'n_cores' in kwargs: n_cores = kwargs['n_cores'] else: n_cores = min(GTConfig.n_cores, 8) fbond_energy = kwargs.get('fbond_energy', None) bbond_energy = kwargs.get('bbond_energy', None) os.environ['OMP_NUM_THREADS'] = str(n_cores) logger.info(f'Using {n_cores} cores for GAP MD') def dynamics_string(): if temp > 0: # default to Langevin NVT return f'Langevin(system, {dt:.1f} * units.fs, {temp} * units.kB, 0.02)' # Otherwise velocity verlet NVE return f'VelocityVerlet(system, {dt:.1f} * units.fs)' if init_temp is None: init_temp = temp # Print a Python script to execute quippy and use ASE to drive the dynamics with open(f'gap.py', 'w') as quippy_script: print( 'from __future__ import print_function', 'import quippy', 'import numpy as np', 'from ase.io import read, write', 'from ase.io.trajectory import Trajectory', 'from ase.md.velocitydistribution import MaxwellBoltzmannDistribution', 'from ase import units', 'from ase.md.langevin import Langevin', 'from ase.md.verlet import VelocityVerlet', 'system = read("config.xyz")', f'system.cell = [{a}, {b}, {c}]', 'system.pbc = True', 'system.center()', f'{gap.ase_gap_potential_str()}', 'system.set_calculator(pot)', ase_momenta_string(configuration, init_temp, bbond_energy, fbond_energy), 'traj = Trajectory("tmp.traj", \'w\', system)\n', 'energy_file = open("tmp_energies.txt", "w")', 'def print_energy(atoms=system):', ' energy_file.write(str(atoms.get_potential_energy())+"\\n")\n', f'dyn = {dynamics_string()}', f'dyn.attach(print_energy, interval={interval})', f'dyn.attach(traj.write, interval={interval})', f'dyn.run(steps={n_steps})', 'energy_file.close()', sep='\n', file=quippy_script) # Run the process quip_md = Popen(GTConfig.quippy_gap_command + ['gap.py'], shell=False, stdout=PIPE, stderr=PIPE) _, err = quip_md.communicate() if len(err) > 0 and 'WARNING' not in err.decode(): logger.error(f'GAP MD: {err.decode()}') traj = Trajectory('tmp.traj', init_configuration=configuration) return traj
def get_active_config_diff(config, gap, temp, e_thresh, max_time_fs, ref_method_name='dftb', curr_time_fs=0, n_calls=0, extra_time_fs=0, **kwargs): """ Given a configuration run MD with a GAP until the absolute error between the predicted and true values is above a threshold -------------------------------------------------------------------------- :param config: (gt.Configuration) :param gap: (gt.GAP) :param e_thresh: (float) Threshold energy error (eV) above which the configuration is returned :param temp: (float) Temperature to propagate GAP-MD :param max_time_fs: (float) :param ref_method_name: (str) :param curr_time_fs: (float) :param n_calls: (int) Number of times this function has been called :param extra_time_fs: (float) Some extra time to run initially e.g. as the GAP is already likely to get to e.g. 100 fs, so run that initially and don't run ground truth evaluations :return: (gt.Configuration) """ if float(temp) < 0: raise ValueError('Cannot run MD with a negative temperature') if float(e_thresh) < 0: raise ValueError(f'Error threshold {e_thresh} must be positive (eV)') if extra_time_fs > 0: logger.info(f'Running an extra {extra_time_fs:.1f} fs of MD before ' f'calculating an error') md_time_fs = 2 + n_calls**3 + float(extra_time_fs) gap_traj = gt.md.run_gapmd(config, gap=gap, temp=float(temp), dt=0.5, interval=4, fs=md_time_fs, n_cores=1, **kwargs) # Actual initial time, given this function can be called multiple times for frame in gap_traj: frame.t0 = curr_time_fs + extra_time_fs # Evaluate the error on the final frame error = calc_error(frame=gap_traj[-1], gap=gap, method_name=ref_method_name) # And the number of ground truth evaluations for this configuration n_evals = n_calls + 1 if error > 100 * e_thresh: logger.error('Huge error: 100x threshold, returning the first frame') gap_traj[0].single_point(method_name=ref_method_name, n_cores=1) gap_traj[0].n_evals = n_evals + 1 return gap_traj[0] if error > 10 * e_thresh: logger.warning('Error 10 x threshold! Taking the last frame less than ' '10x the threshold') # Stride through only 10 frames to prevent very slow backtracking for frame in reversed(gap_traj[::max(1, len(gap_traj)//10)]): error = calc_error(frame, gap=gap, method_name=ref_method_name) n_evals += 1 if e_thresh < error < 10 * e_thresh: frame.n_evals = n_evals return frame if error > e_thresh: gap_traj[-1].n_evals = n_evals return gap_traj[-1] if curr_time_fs + md_time_fs > max_time_fs: logger.info(f'Reached the maximum time {max_time_fs} fs, returning ' f'None') return None # Increment t_0 to the new time curr_time_fs += md_time_fs # If the prediction is within the threshold then call this function again return get_active_config_diff(config, gap, temp, e_thresh, max_time_fs, curr_time_fs=curr_time_fs, ref_method_name=ref_method_name, n_calls=n_calls+1, **kwargs)
def run_dftbmd(configuration, temp, dt, interval, **kwargs): """ Run ab-initio molecular dynamics on a system. To run a 10 ps simulation with a timestep of 0.5 fs saving every 10th step at 300K run_dftbmd(config, temp=300, dt=0.5, interval=10, ps=10) --------------------------------------------------------------------------- :param configuration: (gaptrain.configurations.Configuration) :param temp: (float) Temperature in K to use :param dt: (float) Timestep in fs :param interval: (int) Interval between printing the geometry :param kwargs: {fs, ps, ns} Simulation time in some units """ logger.info('Running DFTB+ MD') ase_atoms = configuration.ase_atoms() if 'n_cores' in kwargs: os.environ['OMP_NUM_THREADS'] = str(kwargs['n_cores']) else: os.environ['OMP_NUM_THREADS'] = str(GTConfig.n_cores) dftb = DFTB(atoms=ase_atoms, kpts=(1, 1, 1), Hamiltonian_Charge=configuration.charge) ase_atoms.set_calculator(dftb) # Do a single point energy evaluation to make sure the calculation works.. # also to generate the input file which can be modified try: ase_atoms.get_potential_energy() except ValueError: raise Exception('DFTB+ failed to calculate the first point') # Append to the generated input file with open('dftb_in.hsd', 'a') as input_file: print('Driver = VelocityVerlet{', f' TimeStep [fs] = {dt}', ' Thermostat = NoseHoover {', f' Temperature [Kelvin] = {temp}', ' CouplingStrength [cm^-1] = 3200', ' }', f' Steps = {simulation_steps(dt, kwargs)}', ' MovedAtoms = 1:-1', f' MDRestartFrequency = {interval}', '}', sep='\n', file=input_file) with open('dftb_md.out', 'w') as output_file: process = Popen([os.environ['DFTB_COMMAND']], shell=False, stderr=PIPE, stdout=output_file) _, err = process.communicate() if len(err) > 0: logger.error(f'DFTB MD: {err.decode()}') return Trajectory('geo_end.xyz', init_configuration=configuration)
def get_active_configs(config, gap, ref_method_name, method='diff', max_time_fs=1000, n_configs=10, temp=300, e_thresh=0.1, min_time_fs=0, **kwargs): """ Generate n_configs using on-the-fly active learning parallelised over GTConfig.n_cores -------------------------------------------------------------------------- :param config: (gt.Configuration) Initial configuration to propagate from :param gap: (gt.gap.GAP) GAP to run MD with :param ref_method_name: (str) Name of the method to use as the ground truth :param method: (str) Name of the strategy used to generate new configurations :param max_time_fs: (float) Maximum propagation time in the active learning loop. Default = 1 ps :param n_configs: (int) Number of configurations to generate :param temp: (float) Temperature in K to run the intermediate MD with :param e_thresh: (float) Energy threshold in eV above which the MD frame is returned by the active learning function i.e E_t < |E_GAP - E_true| method='diff' :param min_time_fs: (float) Minimum propagation time in the active learning loop. If non-zero then will run this amount of time initially then look for a configuration with a |E_0 - E_GAP| > e_thresh :param kwargs: Additional keyword arguments passed to the GAP MD function :return:(gt.ConfigurationSet) """ if int(n_configs) < int(gt.GTConfig.n_cores): raise NotImplementedError('Active learning is only implemented using ' 'one core for each process. Please use ' 'n_configs >= gt.GTConfig.n_cores') results = [] configs = gt.Data() logger.info('Searching for "active" configurations with a threshold of ' f'{e_thresh:.6f} eV') if method.lower() == 'diff': function = get_active_config_diff args = (config, gap, temp, e_thresh, max_time_fs, ref_method_name, 0, 0, min_time_fs) elif method.lower() == 'qbc': function = get_active_config_qbc # Train a few GAPs on the same data gap = gt.gap.GAPEnsemble(name=f'{gap.name}_ensemble', gap=gap) gap.train() args = (config, gap, temp, e_thresh, max_time_fs) elif method.lower() == 'gp_var': function = get_active_config_gp_var args = (config, gap, temp, e_thresh, max_time_fs) else: raise ValueError('Unsupported active method') logger.info(f'Using {gt.GTConfig.n_cores} processes') with Pool(processes=int(gt.GTConfig.n_cores)) as pool: for _ in range(n_configs): result = pool.apply_async(func=function, args=args, kwds=kwargs) results.append(result) for result in results: try: config = result.get(timeout=None) if config is not None and config.energy is not None: configs.add(config) # Lots of different exceptions can be raised when trying to # generate an active config, continue regardless.. except Exception as err: logger.error(f'Raised an exception in calculating the energy\n' f'{err}') continue if method.lower() != 'diff': logger.info('Running reference calculations on configurations ' f'generated by {method}') configs.single_point(method_name=ref_method_name) # Set the number of ground truth function calls for each iteration for config in configs: config.n_evals = 1 return configs