Beispiel #1
0
    def add_perturbation(self, sigma=0.05, max_length=0.2):
        """Add a random perturbation to all atoms in the configuration

        ----------------------------------------------------------------------
        :param sigma: (float) Variance of the normal distribution used to
                      generate displacements in Å

        :param max_length: (float) Maximum length of the random displacement
                           vector in Å
        """
        logger.info(f'Displacing all atoms in the system using a random '
                    f'displacments from a normal distribution:\n'
                    f'σ        = {sigma} Å\n'
                    f'max(|v|) = {max_length} Å')

        for atom in self.atoms:

            # Generate random vectors until one has length < threshold
            while True:
                vector = np.random.normal(loc=0.0, scale=sigma, size=3)

                if np.linalg.norm(vector) < max_length:
                    atom.translate(vector)
                    break

        return None
Beispiel #2
0
    def _set_soap(self, atom_symbols):
        """Set the SOAP parameters"""
        added_pairs = []

        for symbol in set(atom_symbols):

            if symbol == 'H':
                logger.warning('Not adding SOAP on H')
                continue

            params = GTConfig.gap_default_soap_params.copy()

            # Add all the atomic symbols that aren't this one, the neighbour
            # density for which also hasn't been added already
            params["other"] = [
                s for s in set(atom_symbols) if s +
                symbol not in added_pairs and symbol + s not in added_pairs
            ]

            # If there are no other atoms of this type then remove the self
            # pair
            if atom_symbols.count(symbol) == 1:
                params["other"].remove(symbol)

            for other_symbol in params["other"]:
                added_pairs.append(symbol + other_symbol)

            if len(params["other"]) == 0:
                logger.info(f'Not adding SOAP to {symbol} - should be covered')
                continue

            self.soap[symbol] = params

        return None
Beispiel #3
0
    def remove_above_e(self, threshold, min_energy=None):
        """
        Remove configurations above an energy threshold

        :param threshold: (float) Energy threshold in eV
        :param min_energy: (float or None) Minimum/reference energy to use to
                           calculate the relative energy of a configuration. If
                           None then the lowest energy in this set is used
        """
        if any(config.energy is None for config in self._list):
            raise ValueError('Cannot truncate: a config had energy=None')

        if min_energy is None:
            min_energy = min(config.energy for config in self._list)

        logger.info(f'Truncating {len(self._list)} configurations with an '
                    f'energy threshold of {threshold:.3f} eV above a reference'
                    f' energy of {min_energy:.3f} eV')

        self._list = [
            config for config in self._list
            if (config.energy - min_energy) < threshold
        ]

        logger.info(f'Truncated on energies to {len(self._list)}')
        return None
Beispiel #4
0
    def __init__(self,
                 xyz_filename=None,
                 charge=0,
                 spin_multiplicity=1,
                 gmx_itp_filename=None,
                 atoms=None):
        """Molecule e.g. H2O

        -----------------------------------------------------------------------
        :param xyz_filename: (str)

        :param charge: (int)

        :param spin_multiplicity: (int)

        :param gmx_itp_filename: (str) Filename(path) of the GROMACS .itp file
                                 containing MM parameters required to simulate

        :param atoms: (list(autode.atoms.Atom))
        """
        if xyz_filename is not None:
            atoms = xyz_file_to_atoms(xyz_filename)

        super().__init__(charge=charge,
                         spin_multiplicity=spin_multiplicity,
                         atoms=atoms)

        self.itp_filename = gmx_itp_filename
        self.name = str(self)

        logger.info(f'Initialised {self.name}\n'
                    f'Number of atoms      = {self.n_atoms}\n'
                    f'GROMACS itp filename = {self.itp_filename}')
Beispiel #5
0
def simulation_steps(dt, kwargs):
    """Calculate the number of simulation steps

    :param dt: (float) Timestep in fs
    :param kwargs: (dict)
    :return: (float)
    """
    if dt < 0.09 or dt > 5:
        logger.warning('Unexpectedly small or large timestep - is it in fs?')

    if 'ps' in kwargs:
        time_fs = 1E3 * kwargs['ps']

    elif 'fs' in kwargs:
        time_fs = kwargs['fs']

    elif 'ns' in kwargs:
        time_fs = 1E6 * kwargs['ns']

    else:
        raise ValueError('Simulation time not found')

    logger.info(f'Running {time_fs / dt:.0f} steps with a timestep of {dt} fs')
    # Run at least one step
    return max(int(time_fs / dt), 1)
Beispiel #6
0
def remove_intra(configs, gap):
    """
    Remove the intramolecular energy and forces from a set of configurations
    using a GAP

    :param configs: (gt.ConfigurationSet)
    :param gap: (gt.GAP)
    """

    if isinstance(gap, gt.gap.IIGAP):
        logger.info('Removing the intramolecular energy and forces..')
        intra_configs = configs.copy()
        intra_configs.parallel_gap(gap=gap.intra)

        for config, i_config in zip(configs, intra_configs):
            config.energy -= i_config.energy
            config.forces -= i_config.forces

        # If there is also a solute in the system then remove the energy
        # associated with it
        if isinstance(gap, gt.gap.SSGAP):
            solute_configs = configs.copy()
            solute_configs.parallel_gap(gap=gap.solute_intra)

            for config, s_config in zip(configs, solute_configs):
                config.energy -= s_config.energy
                config.forces -= s_config.forces

    return configs
Beispiel #7
0
    def train(self, data=None, sub_sample=False):
        """
        Train the ensemble of GAPS

        :param data: (gaptrain.data.Data)
        :param sub_sample: (bool) Should the data be sub sampled or the full
                           set used?
        """

        if data is None:
            data = self.training_data

        if data is None:
            raise AssertionError('Could not train - no training data set')

        logger.info(f'Training an ensemble with a total of {len(data)} '
                    'configurations')

        for i, gap in enumerate(self.gaps):

            if sub_sample:
                training_data = self.sub_sampled_data(data, gap, random=True)
            else:
                training_data = data.copy()

            # Ensure that the data's name is unique, for saving etc.
            training_data.name += f's{i}'

            # Train the GAP
            gap.train(data=training_data)

        return None
Beispiel #8
0
    def __init__(self,
                 configs,
                 e_lower=0.1,
                 e_thresh=None,
                 max_fs=1000,
                 interval_fs=20,
                 temp=300,
                 dt_fs=0.5):
        """
        τ_acc prospective error metric in fs

        ----------------------------------------------------------------------
        :param configs: (list(gt.Configuration) | gt.ConfigurationSet) A set of
                        initial configurations from which dynamics with be
                        propagated from

        :param e_lower: (float) E_l energy threshold in eV below which
                        the error is zero-ed, i.e. the acceptable level of
                        error possible in the system

        :param e_thresh: (float | None) E_t total cumulative error in eV. τ_acc
                         is defined at the time in the simulation where this
                         threshold is exceeded. If None then:
                         e_thresh = 10 * e_lower

        :param max_fs: (float) Maximum time in femto-seconds for τ_acc

        :zaram interval_fs: (float) Interval between which |E_true - E_GAP| is
                            calculated. *MUST* be at least one timestep

        :param temp: (float) Temperature of the simulation to perform

        :param dt_fs: (float) Timestep of the simulation in femto-seconds
        """
        if len(configs) < 1:
            raise ValueError('Must have at least one configuration to '
                             'calculate τ_acc from')

        if interval_fs < dt_fs:
            raise ValueError('The calculated interval must be more than a '
                             'single timestep')

        self.value = 0  # τ_acc / fs
        self.error = None  # standard error in the mean

        self.init_configs = configs

        self.dt = float(dt_fs)
        self.temp = float(temp)
        self.max_time = float(max_fs)
        self.interval_time = float(interval_fs)

        self.e_l = float(e_lower)
        self.e_t = 10 * self.e_l if e_thresh is None else float(e_thresh)

        logger.info('Successfully initialised τ_acc, will do a maximum of '
                    f'{int(self.max_time // self.interval_time)} reference '
                    f'calculations')
Beispiel #9
0
 def histogram(self, name=None, ref_energy=None):
     """Generate a histogram of the energies and forces in these data"""
     logger.info('Plotting histogram of energies and forces')
     # Histogram |F_ij| rather than the components F_ijk for a force F in on
     # an atom j in a configuration i
     return histogram(self.energies(),
                      self.force_magnitudes(),
                      name=name,
                      ref_energy=ref_energy)
Beispiel #10
0
def set_threads(n_cores):
    """Set the number of threads to use"""

    n_cores = GTConfig.n_cores if n_cores is None else n_cores
    logger.info(f'Using {n_cores} cores')

    os.environ['OMP_NUM_THREADS'] = str(n_cores)
    os.environ['MLK_NUM_THREADS'] = str(n_cores)

    return None
Beispiel #11
0
    def train(self, data):
        """Train the inter-component of the GAP"""
        logger.info('Training the intermolecular component of the potential. '
                    'Expecting data that is free from intra energy and force')

        if not os.path.exists(f'{self.intra.name}.xml'):
            raise RuntimeError('Intra must be already trained')

        self.training_data = data
        return self.inter.train(data)
Beispiel #12
0
def calc_error(frame, gap, method_name):
    """Calculate the error between the ground truth and the GAP prediction"""
    frame.single_point(method_name=method_name,  n_cores=1)

    pred = frame.copy()
    pred.run_gap(gap=gap, n_cores=1)

    error = np.abs(pred.energy - frame.energy)
    logger.info(f'|E_GAP - E_0| = {np.round(error, 3)} eV')
    return error
Beispiel #13
0
def get_active_config_qbc(config, gap, temp, std_e_thresh, max_time_fs, **kwargs):
    """
    Generate an 'active' configuration, i.e. a configuration to be added to the
    training set by active learning, using a query-by-committee model, where
    the prediction between different models (standard deviation) exceeds a
    threshold (std_e_thresh)

    ------------------------------------------------------------------------
    :param config: (gt.Configuration)

    :param gap: (gt.GAPEnsemble) An 'ensemble' of GAPs trained on the same/
                similar data to make predictions with

    :param temp: (float) Temperature for the GAP-MD

    :param std_e_thresh: (float) Threshold for the maximum standard deviation
                         between the GAP predictions (on the whole system),
                         above which a frame is added

    :param max_time_fs: (float)

    :return: (gt.Configuration)
    """
    n_iters, curr_time = 0, 0.0

    while curr_time < max_time_fs:

        gap_traj = gt.md.run_gapmd(config,
                                   gap=gap.gaps[0],
                                   temp=float(temp),
                                   dt=0.5,
                                   interval=4,
                                   fs=2 + n_iters**3,
                                   n_cores=1,
                                   **kwargs)

        for frame in gap_traj[::max(1, len(gap_traj)//10)]:
            pred_es = []
            # Calculated predicted energies in serial for all the gaps
            for single_gap in gap.gaps:
                frame.run_gap(gap=single_gap, n_cores=1)
                pred_es.append(frame.energy)

            # and return the frame if the standard deviation in the predictions
            # is larger than a threshold
            std_e = np.std(np.array(pred_es))
            if std_e > std_e_thresh:
                return frame

            logger.info(f'σ(t={curr_time:.1f}) = {std_e:.6f}')

        n_iters += 1
        curr_time += 2 + n_iters**3

    return None
Beispiel #14
0
    def predict_energy_error(self, *args):
        """
        Predict the standard deviation between predicted energies

        -----------------------------------------------------------------------
        :param args: (gaptrain.configurations.Configuration |
                      gaptrain.configurations.ConfigurationSet)

        :return: (float | list(float)) Error on each of the configurations
        """
        configs = []

        # Populate a list of all the configurations that need to be
        for arg in args:
            try:
                for config in arg:
                    configs.append(config)

            except TypeError:
                assert arg.__class__.__name__ == 'Configuration'
                configs.append(arg)

        assert len(configs) != 0

        start_time = time()
        results = np.empty(shape=(len(configs), self.n_gaps()), dtype=object)
        predictions = np.empty(shape=results.shape)

        os.environ['OMP_NUM_THREADS'] = '1'
        os.environ['MLK_NUM_THREADS'] = '1'
        logger.info('Set OMP and MLK threads to 1')

        with Pool(processes=GTConfig.n_cores) as pool:

            # Apply the method to each configuration in this set
            for i, config in enumerate(configs):
                for j, gap in enumerate(self.gaps):
                    result = pool.apply_async(func=run_gap,
                                              args=(config, None, gap))
                    results[i, j] = result

            # Reset all the configurations in this set with updated energy
            # and forces (each with .true)
            for i in range(len(configs)):
                for j in range(self.n_gaps()):
                    predictions[i, j] = results[i, j].get(timeout=None).energy

        logger.info(f'Calculations done in {(time() - start_time):.1f} s')

        if len(configs) == 1:
            return np.std(predictions[0])

        return [np.std(predictions[i, :]) for i in range(len(configs))]
Beispiel #15
0
    def _set_pairs(self, atom_symbols):
        """Set the two-body pair parameters"""

        for pair in combinations_with_replacement(set(atom_symbols), r=2):

            s_a, s_b = pair  # Atomic symbols of the pair
            if s_a == s_b and atom_symbols.count(s_a) == 1:
                logger.info(f'Only a single {s_b} atom not adding pairwise')
                continue

            self.pairwise[pair] = GTConfig.gap_default_2b_params

        return None
Beispiel #16
0
    def __init__(self, configs_a, configs_b):
        """
        A loss on energies (eV) and forces (eV Å-1). Force loss is computed
        element-wise

        :param configs_a: (gaptrain.configurations.ConfigurationSet)
        :param configs_b: (gaptrain.configurations.ConfigurationSet)
        """
        logger.info(
            f'Calculating loss between {len(configs_a)} configurations')

        self.energy = self.loss(configs_a, configs_b, attr='energy')
        self.force = self.loss(configs_a, configs_b, attr='forces')
Beispiel #17
0
    def save(self, filename=None, append=False):
        """
        Print this configuration as an extended xyz file where the first 4
        columns are the atom symbol, x, y, z and, if this configuration
        contains forces then add the x, y, z components of the force on as
        columns 4-7.

        -----------------------------------------------------------------------
        :param filename: (str)

        :param append: (bool) Append to the end of this exyz file?
        """
        if filename is None:
            filename = f'{self.name}.xyz'
            logger.info(f'Saving configuration as {filename}')

        a, b, c = self.box.size

        energy_str = ''
        if self.energy is not None:
            energy_str += f'dft_energy={self.energy:.8f}'

        prop_str = 'Properties=species:S:1:pos:R:3'
        if self.forces is not None:
            prop_str += ':dft_forces:R:3'

        if not filename.endswith('.xyz'):
            logger.warning('Filename had no .xyz extension - adding')
            filename += '.xyz'

        with open(filename, 'a' if append else 'w') as exyz_file:
            print(
                f'{len(self.atoms)}\n'
                f'Lattice="{a:.6f} 0.000000 0.000000 '
                f'0.000000 {b:.6f} 0.000000 '
                f'0.000000 0.000000 {c:.6f}" '
                f'{prop_str} '
                f'{energy_str}',
                file=exyz_file)

            for i, atom in enumerate(self.atoms):
                x, y, z = atom.coord
                line = f'{atom.label} {x:.5f} {y:.5f} {z:.5f} '

                if self.forces is not None:
                    fx, fy, fz = self.forces[i]
                    line += f'{fx:.5f} {fy:.5f} {fz:.5f}'

                print(line, file=exyz_file)

        return None
Beispiel #18
0
    def truncate(self, n, method='random', **kwargs):
        """
        Truncate this set of configurations to a n configurations

        :param n: (int) Number of configurations to truncate to
        :param method: (str) Name of the method to use
        :param kwargs: ensemble (gaptrain.gap.GAPEnsemble)
        """
        implemented_methods = ['random', 'cur', 'ensemble', 'higher', 'cur_k']

        if method.lower() not in implemented_methods:
            raise NotImplementedError(f'Methods are {implemented_methods}')

        if n > len(self._list):
            raise ValueError(f'Cannot truncate a set of {len(self)} '
                             f'configurations to {n}')

        if method.lower() == 'random':
            return self.remove_random(remainder=n)

        if 'cur' in method.lower():
            if method.lower() == 'cur':
                matrix = gt.descriptors.soap(self)
            elif method.lower() == 'cur_k':
                matrix = gt.descriptors.soap_kernel_matrix(self)
            else:
                raise NotImplementedError

            cur_idxs = gt.cur.rows(matrix, k=n, return_indexes=True)
            self._list = [self._list[idx] for idx in cur_idxs]

        if method.lower() == 'ensemble':
            logger.info(f'Truncating {len(self)} configurations based on the'
                        f' largest errors using a ensemble of GAP models')

            if 'ensemble' not in kwargs:
                raise KeyError('A GAPEnsemble must be provided to use'
                               'ensemble truncation i.e.: '
                               'truncate(.., ensemble=ensemble_instance)')

            errors = kwargs['ensemble'].predict_energy_error(self._list)

            # Use the n configurations with the largest error, where numpy
            # argsort sorts from smallest->largest so take the last n values
            self._list = [self._list[i] for i in np.argsort(errors)[-n:]]

        if method.lower() == 'higher':
            return self.remove_highest_e(n=len(self) - n)

        return None
Beispiel #19
0
    def __init__(self, name, system, molecule):
        """An intramolecular GAP, must be initialised with a system so the
        molecules are defined

        :param name: (str)
        :param system: (gt.system.System)
        """
        super().__init__(name, system)

        self.mol_idxs = []
        self._set_mol_idxs(system, molecule)

        logger.info(f'Initialised an intra-GAP with molecular indexes:'
                    f' {self.mol_idxs}')
Beispiel #20
0
    def __add__(self, other):
        """Add another configuration or set of configurations onto this one"""

        if isinstance(other, Configuration):
            self._list.append(other)

        elif isinstance(other, ConfigurationSet):
            self._list += other._list

        else:
            raise TypeError('Can only add a Configuration or'
                            f' ConfigurationSet, not {type(other)}')

        logger.info(f'Current number of configurations is {len(self)}')
        return self
Beispiel #21
0
def ase_momenta_string(configuration, temp, bbond_energy: dict,
                       fbond_energy: dict):
    """Generate a string to set the initial momenta

    :param configuration: (gt.Configuration)
    :param temp: (float)
    :param bbond_energy: (dict | None) Breaking bond energy
    :param fbond_energy: (dict | None) Forming bond energy
    """

    string = ''

    if temp > 0:
        logger.info(f'Initialising initial velocities for a temperature '
                    f'{temp} K')
        string += f'MaxwellBoltzmannDistribution(system, {temp} * units.kB)\n'

    else:
        # Set the momenta to zero
        string += f"system.arrays['momenta'] = np.zeros((len(system), 3))\n"

    def momenta(idx, vector, energy):
        return (f"system.arrays['momenta'][{int(idx)}] = "
                f"np.sqrt(system.get_masses()[{int(idx)}] * {energy})"
                f" * np.array({vector.tolist()})\n")

    coords = configuration.coordinates()
    if bbond_energy is not None:
        logger.info('Adding breaking bond momenta')

        for atom_idxs, energy in bbond_energy.items():
            i, j = atom_idxs
            logger.info(f'Adding {energy} eV to break bond: {i}-{j}')

            #    vec
            #   <---   i--j         where i and j are two atoms
            #
            vec = coords[i] - coords[j]
            vec /= np.linalg.norm(vec)  # normalise

            string += momenta(idx=i, vector=vec, energy=energy)
            string += momenta(idx=j, vector=-vec, energy=energy)

    if fbond_energy is not None:
        for atom_idxs, energy in fbond_energy.items():
            i, j = atom_idxs
            logger.info(f'Adding {energy} eV to form bond: {i}-{j}')

            #    vec
            #   --->   i--j         where i and j are two atoms
            #
            vec = coords[j] - coords[i]
            vec /= np.linalg.norm(vec)  # normalise

            string += momenta(idx=i, vector=vec, energy=energy)
            string += momenta(idx=j, vector=-vec, energy=energy)

    return string
Beispiel #22
0
    def _calculate_single(self, init_config, gap, method_name):
        """
        Calculate a single τ_acc from one configuration

        :param init_config: (gt.Configuration)
        :param gap: (gt.GAP)
        :param method_name: (str) Ground truth method e.g. dftb, orca, gpaw
        """

        cuml_error, curr_time = 0, 0

        block_time = self.interval_time * gt.GTConfig.n_cores
        step_interval = self.interval_time // self.dt

        while curr_time < self.max_time:

            traj = gt.md.run_gapmd(init_config,
                                   gap=gap,
                                   temp=self.temp,
                                   dt=self.dt,
                                   interval=step_interval,
                                   fs=block_time,
                                   n_cores=min(gt.GTConfig.n_cores, 4))

            # Only evaluate the energy
            try:
                traj.single_point(method_name=method_name)
            except ValueError:
                logger.warning('Failed to calculate single point energies with'
                               f' {method_name}. τ_acc will be underestimated '
                               f'by <{block_time}')
                return curr_time

            pred = traj.copy()
            pred.parallel_gap(gap=gap)

            logger.info('      ___ |E_true - E_GAP|/eV ___')
            logger.info(f' t/fs      err      cumul(err)')

            for j in range(len(traj)):
                e_error = np.abs(traj[j].energy - pred[j].energy)

                # Add any error above the allowed threshold
                cuml_error += max(e_error - self.e_l, 0)
                curr_time += self.dt * step_interval
                logger.info(f'{curr_time:5.0f}     '
                            f'{e_error:6.4f}     '
                            f'{cuml_error:6.4f}')

                if cuml_error > self.e_t:
                    return curr_time

            init_config = traj[-1]

        logger.info(f'Reached max(τ_acc) = {self.max_time} fs')
        return self.max_time
Beispiel #23
0
    def calculate(self, gap, method_name):
        """Calculate the time to accumulate self.e_t eV of error above
        self.e_l eV"""

        taus = []

        for config in self.init_configs:
            tau = self._calculate_single(init_config=config,
                                         gap=gap,
                                         method_name=method_name)
            taus.append(tau)

        # Calculate τ_acc as the average ± the standard error in the mean
        self.value = np.average(np.array(taus))
        if len(taus) > 1:
            self.error = np.std(np.array(taus)) / np.sqrt(len(taus))  # σ / √N

        logger.info(str(self))
        return None
Beispiel #24
0
def soap(*args):
    """
    Create a SOAP vector using dscribe (https://github.com/SINGROUP/dscribe)
    for a set of configurations

    soap(config)           -> [[v0, v1, ..]]
    soap(config1, config2) -> [[v0, v1, ..], [u0, u1, ..]]
    soap(configset)        -> [[v0, v1, ..], ..]

    ---------------------------------------------------------------------------
    :param args: (gaptrain.configurations.Configuration) or
                 (gaptrain.configurations.ConfigurationSet)

    :return: (np.ndarray) shape = (len(args), n) where n is the length of the
             SOAP descriptor
    """
    from dscribe.descriptors import SOAP

    configurations = args

    # If a configuration set is specified then use that as the list of configs
    if len(args) == 1 and isinstance(args[0], Iterable):
        configurations = args[0]

    logger.info(f'Calculating SOAP descriptor for {len(configurations)}'
                f' configurations')

    unique_elements = list(set(atom.label for atom in configurations[0].atoms))

    # Compute the average SOAP vector where the expansion coefficients are
    # calculated over averages over each site
    soap_desc = SOAP(
        species=unique_elements,
        rcut=5,  # Distance cutoff (Å)
        nmax=6,  # Maximum component of the radials
        lmax=6,  # Maximum component of the angular
        average='inner')

    soap_vec = soap_desc.create([conf.ase_atoms() for conf in configurations])

    logger.info('SOAP calculation done')
    return soap_vec
Beispiel #25
0
    def train(self, data):
        """
        Train this GAP on some data

        :param data: (gaptrain.data.Data)
        """
        assert all(config.energy is not None for config in data)
        assert self.params is not None

        if all(
                len(params) == 0
                for params in (self.params.soap, self.params.pairwise,
                               self.params.angle)):
            raise AssertionError('Must have some GAP parameters!')

        logger.info('Training a Gaussian Approximation potential on '
                    f'*{len(data)}* training data points')

        start_time = time()

        self.training_data = data
        self.training_data.save()

        # Run the training using a specified number of total cores
        os.environ['OMP_NUM_THREADS'] = str(GTConfig.n_cores)

        p = Popen(GTConfig.gap_fit_command + self.train_command(),
                  shell=False,
                  stdout=PIPE,
                  stderr=PIPE)
        out, err = p.communicate()

        delta_time = time() - start_time
        print(f'GAP training ran in {delta_time/60:.1f} m')

        if any((delta_time < 0.01, b'SYSTEM ABORT'
                in err, not os.path.exists(f'{self.name}.xml'))):

            raise GAPFailed(f'GAP train errored with:\n '
                            f'{err.decode()}\n'
                            f'{" ".join(self.train_command())}')
        return None
Beispiel #26
0
    def remove_above_f(self, threshold):
        """
        Remove configurations with an atomic force component above a threshold
        value

        :param threshold: (float) Force threshold in eV Å-1
        """
        if any(config.forces is None for config in self._list):
            raise ValueError('Cannot truncate: a config had forces=None')

        logger.info(f'Truncating {len(self._list)} configurations with a '
                    f'force threshold of {threshold:.3f} eV Å-1')

        self._list = [
            config for config in self._list
            if np.max(config.forces) < threshold
        ]

        logger.info(f'Truncated on forces to {len(self._list)}')
        return None
Beispiel #27
0
def set_energy_forces_cp2k_out(configuration, out_filename='cp2k.out'):
    """
    Set the energy and forces of a configuration from a CP2K output file

    :param configuration: (gt.Configuration)
    :param out_filename: (str)
    """
    n_atoms = len(configuration.atoms)
    forces = []

    out_lines = open(out_filename, 'r').readlines()
    for i, line in enumerate(out_lines):
        """
        Total energy:                                      -17.23430883483457
        """
        if 'Total energy:' in line:
            # Convert from Ha to eV
            configuration.energy = ha_to_ev * float(line.split()[-1])

        # And grab the first set of atomic forces
        if 'ATOMIC FORCES' in line:
            logger.info('Found CP2K forces')
            """
            Format e.g.:
            
            ATOMIC FORCES in [a.u.]

            # Atom   Kind   Element          X              Y              Z
              1      1      O           0.02872261     0.00136975     0.02168759
              2      2      H          -0.00988376     0.02251862    -0.01740272
              3      2      H          -0.01791165    -0.02390685    -0.00393702
            """

            for f_line in out_lines[i + 3:i + 3 + n_atoms]:
                fx, fy, fz = f_line.split()[3:]
                forces.append([float(fx), float(fy), float(fz)])
            break

    # Convert from atomic units to eV Å-1
    configuration.forces = np.array(forces) * (ha_to_ev / a0_to_ang)
    return None
Beispiel #28
0
    def remove_highest_e(self, n):
        """
        Remove the highest energy n configurations from this set

        :param n: (int) Number of configurations to remove
        """
        energies = [config.energy for config in self._list]

        if any(energy is None for energy in energies):
            raise ValueError('Cannot remove highest energy from a set '
                             'with some undefined energies')

        if n > len(self._list):
            raise ValueError('The number of configurations needs to be larger '
                             'than the number removed')

        logger.info(f'Removing the least stable {n} configurations')

        idxs = np.argsort(energies)
        self._list = [self._list[i] for i in idxs[:-n]]
        return None
Beispiel #29
0
    def wrap(self, max_wraps=100):
        """
        Wrap all the atoms into the box

        :param max_wraps: (int) Maximum number of recursive calls
        """
        logger.info('Wrapping all atoms back into the box')

        if self.all_atoms_in_box():
            logger.info('All atoms in the box - nothing to be done')
            return None

        for atom in self.atoms:
            for i, _ in enumerate(['x', 'y', 'z']):

                # Atom is not in the upper right octant of 3D space
                if atom.coord[i] < 0:
                    atom.coord[i] += self.box.size[i]

                # Atom is further than the upper right quadrant
                if atom.coord[i] > self.box.size[i]:
                    atom.coord[i] -= self.box.size[i]

        while not self.all_atoms_in_box():
            logger.info('All atoms are still not in the box')
            self.n_wraps += 1

            # Prevent an overflow in the recursive call by setting a threshold
            if self.n_wraps > max_wraps:
                return None

            return self.wrap()

        # Reset the number of wraps performed on this configuration(?)
        return None
Beispiel #30
0
    def __init__(self, name, system=None, n=5, gap=None):
        """
        Ensemble of Gaussian approximation potentials allowing for error
        estimates by sub-sampling

        :param name: (str)
        :param system: (gt.System)
        :param gap: (gt.GAP)
        """
        logger.info(f'Initialising a GAP ensemble with {int(n)} GAPs')
        self.training_data = None

        if system and not gap:
            self.gaps = [GAP(f'{name}_{i}', system) for i in range(int(n))]

        elif gap and not system:
            self.gaps = [deepcopy(gap) for _ in range(int(n))]
            self.training_data = gap.training_data

        else:
            raise AssertionError('Must initialise a GAP ensemble with either '
                                 'a GAP or a System')