Exemple #1
0
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
Exemple #2
0
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
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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