コード例 #1
0
ファイル: rkffile.py プロジェクト: BvB93/PLAMS
    def _write_molecule_section(self, coords, cell, section='Molecule'):
        """
                Write the molecule section
                """
        # Then write the input molecule
        charge = 0.
        element_numbers = [
            PeriodicTable.get_atomic_number(el) for el in self.elements
        ]

        self.file_object.write(section, 'nAtoms', self.ntap)
        self.file_object.write(section, 'AtomicNumbers', element_numbers)
        self.file_object.write(section, 'AtomSymbols', self.elements)
        crd = [
            Units.convert(float(c), 'angstrom', 'bohr') for coord in coords
            for c in coord
        ]
        self.file_object.write(section, 'Coords', crd)
        self.file_object.write(section, 'Charge', charge)
        if cell is not None:
            self.file_object.write(section, 'nLatticeVectors', self.nvecs)
            vecs = [
                Units.convert(float(v), 'angstrom', 'bohr') for vec in cell
                for v in vec
            ]
            self.file_object.write(section, 'LatticeVectors', vecs)
コード例 #2
0
def _(
    self: FromResult[Callable[..., Any]],
    result: CP2K_Result,
    *,
    reduce: None | str | Callable[[Any], Any] = None,
    axis: None | int | tuple[int, ...] = None,
    return_unit: str = 'ha/bohr^3',
    **kwargs: Any,
) -> Any:
    r"""Call :func:`get_bulk_modulus` using argument extracted from **result**.

    Parameters
    ----------
    result : :class:`qmflows.CP2K_Result <qmflows.packages.cp2k_package.CP2K_Result>`
        The Result instance that **self** should operator on.
    reduce : :class:`str` or :class:`Callable[[Any], Any] <collections.abc.Callable>`, optional
        A callback for reducing the output of **self**.
        Alternativelly, one can provide on of the string aliases
        from :attr:`~FromResult.REDUCTION_NAMES`.
    axis : :class:`int` or :class:`Sequence[int] <collections.abc.Sequence>`, optional
        The axis along which the reduction should take place.
        If :data:`None`, use all axes.
    return_unit : :class:`str`
        The unit of the to-be returned quantity.
    \**kwargs : :data:`~typing.Any`
        Further keyword arguments for :func:`get_bulk_modulus`.

    Returns
    -------
    :data:`~typing.Any`
        The output of :func:`get_bulk_modulus`.

    """
    # Attempt to pull from the cache
    if result.status in {'failed', 'crashed'}:
        raise RuntimeError(f"Cannot extract data from a job with status {result.status!r}")

    a_to_au = Units.conversion_ratio('angstrom', 'bohr')
    bar_to_au = Units.conversion_ratio('bar', 'ha/bohr^3')
    volume = self._pop(
        kwargs, 'volume',
        callback=lambda: getattr(result, 'volume') * a_to_au**3,
    )
    pressure = self._pop(
        kwargs, 'pressure',
        callback=lambda: getattr(result, 'pressure') * bar_to_au,
    )

    ret = self(pressure, volume, return_unit=return_unit, **kwargs)
    return self._reduce(ret, reduce, axis)
コード例 #3
0
def get_bulk_modulus(
    pressure: ArrayLike,
    volume: ArrayLike,
    *,
    pressure_unit: str = 'ha/bohr^3',
    volume_unit: str = 'bohr',
    return_unit: str = 'ha/bohr^3',
) -> NDArray[f8]:
    r"""Calculate the bulk modulus via differentiation of **pressure** w.r.t. **volume**.

    .. math::

        B = -V * \frac{\delta P}{\delta V}

    Parameters
    ----------
    pressure : :class:`np.ndarray[np.float64] <numpy.ndarray>`
        A 1D array of pressures used for defining :math:`\delta P`.
        Must be of equal length as **volume**.
    volume : :class:`np.ndarray[np.float64] <numpy.ndarray>`
        A 1D array of volumes used for defining :math:`\delta V`.
        Must be of equal length as **pressure**.
    pressure_unit : :class:`str`
        The unit of the **pressure**.
    volume_unit : :class:`str`
        The unit of the **volume**.
        The passed unit will automatically cubed, *e.g.* ``Angstrom -> Angstrom**3``.
    return_unit : :class:`str`
        The unit of the to-be returned pressure.

    Returns
    -------
    :class:`np.float64 <numpy.double>` or :class:`np.ndarray[np.float64] <numpy.ndarray>`
        The bulk modulus :math:`B`.
        Returend as either a scalar or array, depending on the dimensionality **volume_ref**.


    .. automethod:: get_bulk_modulus.from_result

    """
    # Parse `pressure` and `volume`
    p = np.asarray(pressure, dtype=np.float64) * Units.conversion_ratio(pressure_unit, 'ha/bohr^3')
    v = np.asarray(volume, dtype=np.float64) * Units.conversion_ratio(volume_unit, 'bohr')**3

    ret = np.gradient(p, v)
    ret *= -v
    ret *= Units.conversion_ratio('ha/bohr^3', return_unit)
    return ret
コード例 #4
0
ファイル: ligand_opt.py プロジェクト: pk-organics/CAT
def get_dihed(atoms: Iterable[Atom], unit: str = 'degree') -> float:
    """Return the dihedral angle defined by a set of four atoms.

    Parameters
    ----------
    atoms : |Iterable| [|plams.atoms|]
        An iterable consisting of 4 PLAMS atoms

    unit : :class:`str`
        The output unit.

    Returns
    -------
    :class:`float`
        A dihedral angle expressed in **unit**.

    """
    at1, at2, at3, at4 = atoms
    vec1 = -np.array(at1.vector_to(at2))
    vec2 = np.array(at2.vector_to(at3))
    vec3 = np.array(at3.vector_to(at4))

    v1v2, v2v3 = np.cross(vec1, vec2), np.cross(vec3, vec2)
    v1v2_v2v3 = np.cross(v1v2, v2v3)
    v2_norm_v2 = vec2 / np.linalg.norm(vec2)
    epsilon = np.arctan2(v1v2_v2v3 @ v2_norm_v2, v1v2 @ v2v3)

    return Units.convert(epsilon, 'radian', unit)
コード例 #5
0
def read_multi_xyz(filename, return_comment=True, unit='angstrom'):  # noqa: E302
    r"""Read a (multi) .xyz file.

    Parameters
    ----------
    filename : str
        The path+filename of a (multi) .xyz file.
    return_comment : bool
        Whether or not the comment line in each Cartesian coordinate block should be returned.
        Returned as a 1D array of strings.
    unit : :class:`str`
        The unit of the to-be returned array.

    Returns
    -------
    :math:`m*n*3` |np.ndarray|_ [|np.float64|_], |dict|_ [|str|_, |list|_ [|int|_]] and\
    (optional) :math:`m` |np.ndarray|_ [|str|_]:
        * A 3D array with Cartesian coordinates of :math:`m` molecules with :math:`n` atoms.
        * A dictionary with atomic symbols as keys and lists of matching atomic indices as values.
        * (Optional) a 1D array with :math:`m` comments.

    Raises
    ------
    :exc:`.XYZError`
        Raised when issues are encountered related to parsing .xyz files.

    """
    # Define constants and construct a dictionary: {atomic symbols: [atomic indices]}
    with open(filename, 'r') as f:
        atom_count = _get_atom_count(f)
        idx_dict = _get_idx_dict(f, atom_count)
        try:
            line_count = _get_line_count(f, add=[2, atom_count])
        except UnboundLocalError:  # The .xyz file contains a single molecule
            line_count = 2 + atom_count

    # Check if mol_count is fractional, smaller than 1 or if atom_count is smaller than 1
    mol_count = line_count / (2 + atom_count)
    validate_xyz(mol_count, atom_count, filename)

    # Create the to-be returned xyz array
    shape = int(mol_count), atom_count, 3
    with open(filename, 'r') as f:
        iterator = chain.from_iterable(_xyz_generator(f, atom_count))
        try:
            xyz = np.fromiter(iterator, dtype=float, count=np.product(shape))
        except ValueError as ex:  # Failed to parse the .xyz file
            raise XYZError(str(ex)).with_traceback(ex.__traceback__)
    xyz.shape = shape  # From 1D to 3D array

    if unit != 'angstrom':
        xyz *= Units.conversion_ratio('angstrom', unit)

    if return_comment:
        return xyz, idx_dict, get_comments(filename, atom_count)
    else:
        return xyz, idx_dict
コード例 #6
0
 def _set_prm_pairs(self,
                    atom_pair_mapping: Mapping[Tuple[str, str], float],
                    key: str,
                    unit: Optional[str] = None) -> None:
     unit2au = 1 if unit is None else Units.conversion_ratio(unit, 'au')
     for _at_tup, value in atom_pair_mapping.items():
         at_tup = tuple(sorted(_at_tup))
         value *= unit2au
         self.at[at_tup, key] = value
コード例 #7
0
def get_free_energy(distribution: A,
                    temperature: float = 298.15,
                    unit: str = 'kcal/mol',
                    inf_replace: Optional[float] = np.nan) -> A:
    r"""Convert a distribution function into a free energy function.

    Given a distribution function :math:`g(r)`, the free energy
    :math:`F(g(r))` can be retrieved using a Boltzmann inversion:

    .. math::

        F(g(r)) = -RT * \text{ln} (g(r))

    Two examples of valid distribution functions would be the
    radial-  and angular distribution functions.

    .. _`scm.plams.units`: https://www.scm.com/doc/plams/components/utils.html#scm.plams.tools.units.Units

    Parameters
    ----------
    distribution : array-like
        A distribution function (*e.g.* an RDF) as an array-like object.

    temperature : :class:`float`
        The temperature in Kelvin.

    inf_replace : :class:`float`, optional
        A value used for replacing all instances of infinity (``np.inf``).

    unit : :class:`str`
        The to-be returned unit.
        See `scm.plams.Units`_ for a comprehensive overview of all allowed values.

    Returns
    -------
    :class:`pandas.DataFrame`:
        An array-like object with a free-energy function (kj/mol) of **distribution**.

    See Also
    --------
    :meth:`.MultiMolecule.init_rdf`
        Initialize the calculation of radial distribution functions (RDFs).

    :meth:`.MultiMolecule.init_adf`
        Initialize the calculation of distance-weighted angular distribution functions (ADFs).

    """  # noqa
    RT = (constants.R / 1000) * temperature  # kj/mol

    with np.errstate(divide='ignore'):
        ret = -RT * np.log(distribution)
    if inf_replace is not None:
        ret[ret == np.inf] = inf_replace

    ret *= Units.conversion_ratio('kj/mol', unit)
    return ret
コード例 #8
0
def _fill_uff(psf: PSFContainer, lj: pd.DataFrame) -> None:
    """Fill in all missing core/ligand lennard-jones parameters with those from UFF."""
    epsilon = 'epsilon' in lj.index
    sigma = 'sigma' in lj.index

    # Skip these keys in the settings
    skip = {'unit', 'param'}

    # Convertion ratio between units
    if 'unit' not in lj:
        lj['unit'] = None
    if sigma:
        _sigma_unit = lj.at['sigma', 'unit'] or 'angstrom'
        sigma_unit = Units.conversion_ratio('angstrom', UNIT_MAP[_sigma_unit])
    if epsilon:
        _epsilon_unit = lj.at['epsilon', 'unit'] or 'kcalmol'
        epsilon_unit = Units.conversion_ratio('kcal/mol',
                                              UNIT_MAP[_epsilon_unit])

    # Identify the core and ligand atoms
    is_core = psf.residue_name == 'COR'
    core_at = dict(psf.atoms.loc[is_core,
                                 ['atom type', 'atom name']].values.tolist())
    lig_at = dict(psf.atoms.loc[~is_core,
                                ['atom type', 'atom name']].values.tolist())
    atom_pairs = {frozenset(at.split()) for at in lj.columns if at not in skip}

    for at1, symbol1 in core_at.items():
        for at2, symbol2 in lig_at.items():
            at_set = {at1, at2}
            if at_set in atom_pairs:
                continue

            atom_pairs.add(frozenset(at_set))
            key = '{} {}'.format(*at_set)

            lj[key] = 0.0
            if sigma:
                lj.at['sigma',
                      key] = combine_sigma(symbol1, symbol2) * sigma_unit
            if epsilon:
                lj.at['epsilon',
                      key] = combine_epsilon(symbol1, symbol2) * epsilon_unit
コード例 #9
0
def _charge_test(rdf: pd.DataFrame,
                 charge_dict: Mapping[str, float],
                 temperature: float = 298.15) -> pd.Series:
    def iter_epsilon(G: pd.DataFrame) -> Generator[float, None, None]:
        for at_pair in G:
            at1, at2 = at_pair.split()
            q1q2 = charge_dict.get(at1, 0.0) * charge_dict.get(at2, 0.0)
            r_min = G[at_pair].idxmin()
            E_min = G.at[r_min, at_pair]
            yield -1 * (E_min - q1q2 / r_min)

    G = get_free_energy(rdf, temperature)
    G *= Units.conversion_ratio('kj/mol', 'au')
    G.index *= Units.conversion_ratio('Angstrom', 'Bohr')

    # Prepare the paramater epsilon; correct for Coulombic interaction if necessary
    ret = np.fromiter(iter_epsilon(G), count=G.shape[1], dtype=float)
    ret *= Units.conversion_ratio('au', 'kj/mol')
    return pd.Series(ret, index=G.columns, name='epsilon (kj/mol)')
コード例 #10
0
 def _set_prm(self,
              atom_mapping: Mapping[str, float],
              key: str,
              func: Callable[[Tuple[float, float]], float],
              unit: Optional[str] = None) -> None:
     unit2au = 1 if unit is None else Units.conversion_ratio(unit, 'au')
     atom_pairs = combinations_with_replacement(sorted(atom_mapping.keys()),
                                                2)
     for at1, at2 in atom_pairs:
         value = func((atom_mapping[at1], atom_mapping[at2]))
         value *= unit2au
         self.at[(at1, at2), key] = value
コード例 #11
0
def get_entropy(mol: Molecule, temp: float = 298.15) -> Tuple[float, float]:
    """Calculate the translational of the passsed molecule.

    Parameters
    ----------
    mol : :class:`~scm.plams.mol.molecule.Molecule`
        A PLAMS molecule.
    temp : :class:`float`
        The temperature in Kelvin.

    Returns
    -------
    :class:`float` & :class:`float`
        Two floats respectively representing the translational and rotational entropy.
        Units are in kcal/mol/K

    """
    # Define constants (SI units)
    pi = np.pi
    kT = 1.380648 * 10**-23 * temp  # Boltzmann constant * temperature
    h = 6.6260701 * 10**-34  # Planck constant
    R = 8.31445  # Gas constant
    # Volume(1 mol ideal gas) / Avogadro's number
    V_Na = ((R * temp) / 10**5) / Units.constants['NA']

    mass: np.ndarray = np.array([at.mass for at in mol]) * 1.6605390 * 10**-27
    x, y, z = mol.as_array().T * 10**-10

    # Calculate the rotational partition function: q_rot
    inertia = np.array(
        [[sum(mass * (y**2 + z**2)), -sum(mass * x * y), -sum(mass * x * z)],
         [-sum(mass * x * y),
          sum(mass * (x**2 + z**2)), -sum(mass * y * z)],
         [-sum(mass * x * z), -sum(mass * y * z),
          sum(mass * (x**2 + y**2))]])
    inertia_product = np.product(np.linalg.eig(inertia)[0])
    q_rot = pi**0.5 * ((8 * pi**2 * kT) / h**2)**1.5 * inertia_product**0.5

    # Calculate the translational and rotational entropy in j/mol
    S_trans: np.float64 = 1.5 + np.log(V_Na *
                                       ((2 * pi * sum(mass) * kT) / h**2)**1.5)
    S_rot: np.float64 = 1.5 + np.log(q_rot)

    # Apply the unit
    ret: np.ndarray = np.array([S_trans, S_rot]) * R
    ret *= Units.conversion_ratio('kj/mol', 'kcal/mol') / 1000
    return EntropyTuple(ret.item(0), ret.item(1))
コード例 #12
0
def get_cp2k_thermo(file_name: PathLike, quantity: Quantity = 'G',
                    unit: str = 'kcal/mol') -> float:
    """Return thermochemical properties as extracted from a CP2K .out file.

    Note
    ----
    Note that CP2K can under certain circumstances report entropies and, by extension,
    Gibbs free energies as :data:`~math.nan`.

    Parameters
    ----------
    file_name : :class:`str`, :class:`bytes` or :class:`os.PathLike`
        A path-like object pointing to the CP2K .out file.

    quantity : :class:`str`
        The to-be returned quantity.
        Accepted values are ``"E"``, ``"ZPE"``, ``"H"``, ``"S"`` and ``"G"``.

    unit : :class:`str`
        The unit of the to-be returned *quantity*.
        See :class:`plams.Units<scm.plams.tools.units.Units>` for more details.

    Returns
    -------
    :class:`float`
        A user-specified *quantity* expressed in *unit* as extracted from *file_name*.

    """
    quantity = quantity.upper()
    if quantity not in QUANTITY_SET:
        raise ValueError(f"'quantity' has an invalid value ({quantity!r}); "
                         f"expected values: {tuple(QUANTITY_SET)!r}")

    parser = ZeroOrMore(Suppress(SkipTo(" VIB|              Temperature ")) + SkipTo('\n\n\n\n'))

    energy = next(iter(parser.parseFile(file_name)))
    energy_iter = (i.rsplit(maxsplit=1) for i in energy.splitlines() if i)
    energy_dict = {QUANTITY_MAPPING.get(k): float(v) for k, v in energy_iter}

    energy_dict['H'] += energy_dict['E']
    energy_dict['ZPE'] += energy_dict['E']
    energy_dict['G'] = energy_dict['H'] - energy_dict['T'] * energy_dict['S']

    return energy_dict[quantity] * Units.conversion_ratio('kj/mol', unit)
コード例 #13
0
ファイル: rkffile.py プロジェクト: BvB93/PLAMS
#!/usr/bin/env python

import numpy

from scm.plams import KFFile, Molecule, Bond, AMSResults, Units, PeriodicTable, PlamsError
from .trajectoryfile import TrajectoryFile

__all__ = ['RKFTrajectoryFile']

bohr_to_angstrom = Units.conversion_ratio('bohr', 'angstrom')


class RKFTrajectoryFile(TrajectoryFile):
    """
        Class that represents an RKF file
        """
    def __init__(self, filename, mode='rb', fileobject=None, ntap=None):
        """
                Creates an RKF file object
                """
        self.position = 0
        if filename is not None:
            fileobject = KFFile(filename, autosave=False)
            #fileobject = KFFile(filename,autosave=False,fastsave=True)
            if fileobject is None:
                raise PlamsError('KFFile %s not found.' % (rkfname))
        self.file_object = fileobject
        self.mode = mode

        self.ntap = 0
        if ntap is not None:
コード例 #14
0
def get_pressure(
    forces: ArrayLike,
    coords: ArrayLike,
    volume: ArrayLike,
    temp: float = 298.15,
    *,
    forces_unit: str = 'ha/bohr',
    coords_unit: str = 'bohr',
    volume_unit: str = 'bohr',
    return_unit: str = 'ha/bohr^3',
) -> NDArray[f8]:
    r"""Calculate the pressure from the passed **forces**.

    .. math::

        P = \frac{Nk_{B}T}{V} + \frac{1}{6V}
            \sum_i^N \sum_j^N {\boldsymbol{r}_{ij} \cdot \boldsymbol{f}_{ij}}

    Parameters
    ----------
    forces : :class:`np.ndarray[np.float64] <numpy.ndarray>`, shape :math:`(n_{\text{mol}}, n_{\text{atom}}, 3)`
        A 3D array containing the forces of all molecules within the trajectory.
    coords : :class:`np.ndarray[np.float64] <numpy.ndarray>`, shape :math:`(n_{\text{mol}}, n_{\text{atom}}, 3)`
        A 3D array containing the coordinates of all molecules within the trajectory.
    volume : :class:`np.ndarray[np.float64] <numpy.ndarray>`, shape :math:`(n_{\text{mol}},)`
        A 1D array containing the cell volumes across the trajectory.
    temp : :class:`np.ndarray[np.float64] <numpy.ndarray>`, shape :math:`(n_{\text{mol}},)`
        A 1D array of the temperatures across the trajectory.
    forces_unit : :class:`str`
        The unit of the **forces**.
    coords_unit : :class:`str`
        The unit of the **coords**.
    volume_unit : :class:`str`
        The unit of the **volume**.
        The passed unit will automatically cubed, *e.g.* ``Angstrom -> Angstrom**3``.
    return_unit : :class:`str`
        The unit of the to-be returned pressure.

    Returns
    -------
    :class:`np.ndarray[np.float64] <numpy.ndarray>`, shape :math:`(n_{\text{mol}},)`
        A 1D array with all pressures across the trajectory.


    .. automethod:: get_pressure.from_result

    """  # noqa: E501
    _f = np.asarray(forces, dtype=np.float64) * Units.conversion_ratio(
        forces_unit, 'ha/bohr')
    xyz = np.asarray(coords, dtype=np.float64) * Units.conversion_ratio(
        coords_unit, 'bohr')
    v = np.asarray(volume, dtype=np.float64) * Units.conversion_ratio(
        volume_unit, 'bohr')**3
    t = np.asarray(temp, dtype=np.float64)
    k = Units.convert(constants.Boltzmann, 'j', 'hartree')

    a = (xyz.shape[-2] * k * t) / v
    b = np.empty(len(xyz), dtype=np.float64)

    shape = xyz.shape + (_f.shape[1], )
    iterator = slice_iter(shape, itemsize=b.dtype.itemsize)
    for slc in iterator:
        f = _f[slc, ..., None, :] + _f[slc, ..., None, :, :]
        r = abs(xyz[slc, ..., None, :] - xyz[slc, ..., None, :, :])
        b[slc] = np.einsum("...ijk,...ijk->...ij", r, f).sum(axis=(-1, -2))

    b /= 6 * v
    return (a + b) * Units.conversion_ratio('ha/bohr^3', return_unit)
コード例 #15
0
def guess_param(
    mol_list: Iterable[MultiMolecule],
    param: ParamKind,
    mode: ModeKind = 'rdf',
    *,
    cp2k_settings: None | MutableMapping = None,
    prm: None | PathType | PRMContainer = None,
    psf_list: None | Iterable[PathType | PSFContainer] = None,
    unit: None | str = None,
    param_mapping: None | Mapping[tuple[str, str], float] = None,
) -> Dict[Tuple[str, str], float]:
    """Estimate all Lennard-Jones missing forcefield parameters.

    Examples
    --------
    .. code:: python

        >>> from FOX import MultiMolecule
        >>> from FOX.armc import guess_param

        >>> mol_list = [MultiMolecule(...), ...]
        >>> prm = str(...)
        >>> psf_list = [str(...), ...]

        >>> epsilon_dict = guess_ParamKind(mol_list, 'epsilon', prm=prm, psf_list=psf_list)
        >>> sigma_dict = guess_ParamKind(mol_list, 'sigma', prm=prm, psf_list=psf_list)


    ParamKindeters
    ----------
    mol_list : :class:`Iterable[FOX.MultiMolecule] <collections.abc.Iterable>`
        An iterable of molecules.
    param : :class:`str`
        The to-be estimated parameter.
        Accepted values are ``"epsilon"`` and ``"sigma"``.
    mode : :class:`str`
        The procedure for estimating the parameters.
        Accepted values are ``"rdf"``, ``"uff"``, ``"crystal_radius"`` and ``"ion_radius"``.
    cp2k_settings : :class:`~collections.abc.MutableMapping`, optional
        The CP2K input settings.
    prm : :term:`python:path-like` or :class:`~FOX.PRMContainer`, optional
        An optional .prm file.
    psf_list : :class:`Iterable[str|FOX.PSFContainer] <collections.abc.Iterable>`, optional
        An optional list of .psf files.
    unit : :class:`str`, optional
        The unit of the to-be returned quantity.
        If ``None``, default to kcal/mol for :code:`param="epsilon"`
        and angstrom for :code:`param="sigma"`.

    Returns
    -------
    :class:`dict[tuple[str, str], float] <dict>`
        A dictionary with atom-pairs as keys and the estimated parameters as values.

    """  # noqa: E501
    # Validate param and mode
    param = _validate_arg(param, name='param', ref={'epsilon',
                                                    'sigma'})  # type: ignore
    mode = _validate_arg(mode, name='mode', ref=MODE_SET)  # type: ignore
    if unit is not None:
        convert_unit = Units.conversion_ratio(DEFAULT_UNIT[param], unit)
    else:
        convert_unit = 1

    # Construct a set with all valid atoms types
    mol_list = [mol.copy() for mol in mol_list]
    if psf_list is not None:
        atoms: Set[str] = set()
        for mol, p in zip(mol_list, psf_list):
            psf: PSFContainer = PSFContainer.read(p) if not isinstance(
                p, PSFContainer) else p
            mol.atoms_alias = psf.to_atom_alias_dict()
            atoms |= set(psf.atom_type)
    else:
        atoms = set(chain.from_iterable(mol.atoms.keys() for mol in mol_list))

    # Construct a DataFrame and update it with all available parameters
    df = LJDataFrame(np.nan, index=atoms)
    if cp2k_settings is not None:
        df.overlay_cp2k_settings(cp2k_settings)

    if param_mapping is not None:
        for k, v in param_mapping.items():
            df.loc[k, param] = v / convert_unit

    if prm is not None:
        prm_: PRMContainer = prm if isinstance(
            prm, PRMContainer) else PRMContainer.read(prm)
        df.overlay_prm(prm_)
        prm_dict = _nb_from_prm(prm_, param=param)
    else:
        prm_dict = {}

    # Extract the relevant parameter Series
    _series = df[param]
    series = _series[_series.isnull()]

    # Construct the to-be returned series and set them to the correct units
    ret = _guess_param(series, mode, mol_list=mol_list, prm_dict=prm_dict)
    ret *= convert_unit
    return ret
コード例 #16
0
def get_cp2k_freq(file: Union[PathLike, TextIOBase],
                  unit: str = 'cm-1', **kwargs: Any) -> np.ndarray:
    r"""Extract vibrational frequencies from *file*, a CP2K .mol file in the Molden format.

    Paramters
    ---------
    file : :class:`str`, :class:`bytes`, :class:`os.PathLike` or :class:`io.IOBase`
        A `path- <https://docs.python.org/3/glossary.html#term-path-like-object>`_ or
        `file-like <https://docs.python.org/3/glossary.html#term-file-object>`_ object
        pointing to the CP2K .mol file.
        Note that passed file-like objects should return strings (not bytes) upon iteration;
        consider wrapping *file* in :func:`codecs.iterdecode` if its iteration will yield bytes.

    unit : :class:`str`
        The output unit of the vibrational frequencies.
        See :class:`plams.Units<scm.plams.tools.units.Units>` for more details.

    /**kwargs : :data:`~typing.Any`
        Further keyword arguments for :func:`open`.
        Only relevant if *file* is a path-like object.

    Returns
    -------
    :class:`numpy.ndarray` [:class:`float`], shape :math:`(n,)`
        A 1D array of length :math:`n` containing the vibrational frequencies
        extracted from *file*.

    """
    context_manager = file_to_context(file, **kwargs)

    with context_manager as f:
        item = next(f)
        if not isinstance(item, str):
            raise TypeError(f"Iteration through {f!r} should yield strings; "
                            f"observed type: {item.__class__.__name__!r}")

        # Find the start of the [Atoms] block
        elif '[Atoms]' not in item:
            for item in f:
                if '[Atoms]' in item:
                    break
            else:
                raise ValueError(f"failed to identify the '[Atoms]' substring in {f!r}")

        # Find the end of the [Atoms] block, i.e. the start of the [FREQ] block
        for atom_count, item in enumerate(f):
            if '[FREQ]' in item:
                break
        else:
            raise ValueError(f"failed to identify the '[FREQ]' substring in {f!r}")

        # Identify the vibrational degrees of freedom
        if atom_count == 0:
            raise ValueError(f"failed to identify any atoms in the '[Atoms]' section of {f!r}")
        elif atom_count <= 2:
            count = atom_count - 1
        else:
            count = 3 * atom_count - 6

        # Gather and return the frequencies
        iterator = islice(f, 0, count)
        ret = np.fromiter(iterator, dtype=float, count=count)
        ret *= Units.conversion_ratio('cm-1', unit)
        return ret
コード例 #17
0
def get_V(mol: MultiMolecule,
          slice_mapping: SliceMapping,
          prm_mapping: PrmMapping,
          ligand_count: int,
          core_atoms: Optional[Iterable[str]] = None,
          distance_upper_bound: float = np.inf,
          k: Optional[int] = 20,
          shift_cutoff: bool = True) -> Tuple[pd.DataFrame, pd.DataFrame]:
    r"""Calculate all non-covalent interactions averaged over all molecules in **mol**.

    Parameters
    ----------
    mol : :class:`MultiMolecule`
        A MultiMolecule instance.

    slice_mapping : :class:`dict`
        A mapping of atoms-pairs to matching atomic indices.

    prm_mapping : :class:`dict`
        A mapping of atoms-pairs to matching (pair-wise) values for :math:`q`,
        :math:`sigma` and :math:`\varepsilon`.
        Units should be in atomic units.

    ligand_count : :class:`int`
        The number of ligands.

    core_atoms : :class:`set` [:class:`str`], optional
        A set of all atoms within the core.

    distance_upper_bound : :class:`float`
        Consider only atom-pairs within this distance.

    k : :class:`int`
        The (maximum) number of to-be considered atom-pairs.
        Only relevant when **distance_upper_bound** is not set ``inf``.

    shift_cutoff : :class:`bool`
        Shift all potentials by a constant such that
        it is equal to zero at **distance_upper_bound**.
        Only relavent when ``distance_upper_bound < inf``.

    Returns
    -------
    :class:`pandas.DataFrame` & :class:`pandas.DataFrame`
        Two DataFrames with, respectivelly, the electrostatic and Lennard-Jones components of the
        (inter-ligand) potential energy per atom-pair.
        The potential energy is summed over atoms with matching atom types.
        Units are in atomic units.

    """
    core_atoms = set(core_atoms) if core_atoms is not None else set()
    mol = mol * Units.conversion_ratio('Angstrom',
                                       'au')  # type: ignore[assignment]

    index = pd.RangeIndex(0, len(mol), name='MD Iteration')
    columns = pd.MultiIndex.from_tuples(sorted(slice_mapping.keys()),
                                        names=['atom1', 'atom2'])

    elstat_df = pd.DataFrame(0.0, index=index.copy(), columns=columns.copy())
    lj_df = pd.DataFrame(0.0, index=index, columns=columns)

    # Specify the function for calculating the distance matrices
    if np.isinf(distance_upper_bound):
        dist_func = _get_dist
        k = None
    else:
        dist_func = functools.partial(
            _get_kd_dist, k=k, distance_upper_bound=distance_upper_bound)

    if distance_upper_bound < np.inf and shift_cutoff:
        shift: Optional[float] = distance_upper_bound * Units.conversion_ratio(
            'angstrom', 'au')
    else:
        shift = None

    for atoms, ij in slice_mapping.items():
        charge, epsilon, sigma = prm_mapping[atoms]
        contains_core = bool(core_atoms.intersection(atoms))

        # Construct a :class:`slice` iterator based on the expected array size.
        # Precaution against creating arrays too large to hold in memory
        dmat_size = len(ij[0]) * (k or len(ij[1]))
        slice_iterator = _get_slice_iterator(len(mol), dmat_size)

        # Construct the distance matrices and calculate the potential energies
        for mol_subset in slice_iterator:
            dist = dist_func(mol,
                             ij,
                             ligand_count,
                             contains_core,
                             mol_subset=mol_subset)
            elstat_df.loc[elstat_df.index[mol_subset],
                          atoms] = get_V_elstat(charge,
                                                dist,
                                                shift_cutoff=shift)
            lj_df.loc[lj_df.index[mol_subset],
                      atoms] = get_V_lj(sigma,
                                        epsilon,
                                        dist,
                                        shift_cutoff=shift)
            del dist

        if atoms[0] == atoms[1]:  # Avoid double-counting
            elstat_df[atoms] /= 2
            lj_df[atoms] /= 2

    return elstat_df, lj_df
コード例 #18
0
def get_global_descriptors(
        results: Union[ADFResults, ADF_Result]) -> pd.Series:
    """Extract a dictionary with all ADF conceptual DFT global descriptors from **results**.

    Examples
    --------
    .. code:: python

        >>> import pandas as pd
        >>> from scm.plams import ADFResults
        >>> from CAT.recipes import get_global_descriptors

        >>> results = ADFResults(...)

        >>> series: pd.Series = get_global_descriptors(results)
        >>> print(dct)
        Electronic chemical potential (mu)     -0.113
        Electronegativity (chi=-mu)             0.113
        Hardness (eta)                          0.090
        Softness (S)                           11.154
        Hyperhardness (gamma)                  -0.161
        Electrophilicity index (w=omega)        0.071
        Dissocation energy (nucleofuge)         0.084
        Dissociation energy (electrofuge)       6.243
        Electrodonating power (w-)              0.205
        Electroaccepting power(w+)              0.092
        Net Electrophilicity                    0.297
        Global Dual Descriptor Deltaf+          0.297
        Global Dual Descriptor Deltaf-         -0.297
        Electronic chemical potential (mu+)    -0.068
        Electronic chemical potential (mu-)    -0.158
        Name: global descriptors, dtype: float64


    Parameters
    ----------
    results : :class:`plams.ADFResults` or :class:`qmflows.ADF_Result`
        A PLAMS Results or QMFlows Result instance of an ADF calculation.

    Returns
    -------
    :class:`pandas.Series`
        A Series with all ADF global decsriptors as extracted from **results**.

    """
    if not isinstance(results, Results):
        results = results.results
    file = results['$JN.out']

    with open(file) as f:
        # Identify the GLOBAL DESCRIPTORS block
        for item in f:
            if item == ' GLOBAL DESCRIPTORS\n':
                next(f)
                next(f)
                break
        else:
            raise ValueError(
                f"Failed to identify the 'GLOBAL DESCRIPTORS' block in {file!r}"
            )

        # Extract the descriptors
        ret = {}
        for item in f:
            item = item.rstrip('\n')
            if not item:
                break

            _key, _value = item.rsplit('=', maxsplit=1)
            key = _key.strip()
            try:
                value = float(_value)
            except ValueError:
                value = float(_value.rstrip('(eV)')) * Units.conversion_ratio(
                    'ev', 'au')
            ret[key] = value

    # Fix the names of "mu+" and "mu-"
    ret['Electronic chemical potential (mu+)'] = ret.pop('mu+', nan)
    ret['Electronic chemical potential (mu-)'] = ret.pop('mu-', nan)
    return pd.Series(ret, name='global descriptors')
コード例 #19
0
def get_intra_non_bonded(mol: Union[str, MultiMolecule], psf: Union[str, PSFContainer],
                         prm: Union[str, PRMContainer],
                         distance_upper_bound: float = np.inf,
                         shift_cutoff: bool = True,
                         el_scale14: float = 1.0,
                         lj_scale14: float = 1.0) -> Tuple[pd.DataFrame, pd.DataFrame]:
    r"""Collect forcefield parameters and calculate all non-covalent intra-ligand interactions in **mol**.

    Forcefield parameters (*i.e.* charges and Lennard-Jones :math:`\sigma` and
    :math:`\varepsilon` values) are collected from the provided **psf** and **prm** files.

    Inter-ligand, core-core and intra-core interactions are ignored.

    Parameters
    ----------
    mol : :class:`str` or :class:`MultiMolecule`
        A MultiMolecule instance or the path+filename of an .xyz file.

    psf : :class:`str` or :class:`PSFContainer`
        A PSFContainer instance or the path+filename of a .psf file.
         Used for setting :math:`q` and creating atom-subsets.

    prm : :class:`str` or :class:`PRMContainer`
        A PRMContainer instance or the path+filename of a .prm file.
        Used for setting :math:`\sigma` and :math:`\varepsilon`.

    distance_upper_bound : :class:`float`
        Consider only atom-pairs within this distance.
        Using ``inf`` will default to the full, untruncated, distance matrix.

    shift_cutoff : :class:`bool`
        Shift all potentials by a constant such that
        it is equal to zero at **distance_upper_bound**.
        Only relavent when ``distance_upper_bound < inf``.
        Serves the same purpose as the cp2k ``SHIFT_CUTOFF`` keyword.

    el_scale14 : :class:`float`
        Scaling factor to apply to all 1,4-nonbonded electrostatic interactions.
        Serves the same purpose as the cp2k ``EI_SCALE14`` keyword.

    lj_scale14 : :class:`float`
        Scaling factor to apply to all 1,4-nonbonded Lennard-Jones interactions.
        Serves the same purpose as the cp2k ``VDW_SCALE14`` keyword.

    Returns
    -------
    :class:`pandas.DataFrame` & :class:`pandas.DataFrame`
        Two DataFrames with, respectivelly, the electrostatic and Lennard-Jones components of the
        (intra--ligand) potential energy per atom-pair.
        The potential energy is summed over atoms with matching atom types.
        Units are in atomic units.

    """  # noqa
    if not isinstance(psf, PSFContainer):
        psf = PSFContainer.read(psf)

    if not isinstance(mol, MultiMolecule):
        mol_ = MultiMolecule.from_xyz(mol)
    else:
        mol_ = mol.copy(deep=False)

    # Define the various non-bonded atom-pairs
    core_atoms = psf.atoms.index[psf.residue_id == 1] - 1
    lig_atoms = psf.atoms.index[psf.residue_id != 1] - 1
    mol_.bonds = psf.bonds - 1

    # Ensure that PLAMS more or less recognizes the new (custom) atomic symbols
    values = psf.atoms[['atom type', 'atom name']].values
    PT.symtonum.update({k.capitalize(): PT.get_atomic_number(v) for k, v in values})

    # Construct the parameter DataFrames
    mol_.atoms = psf.to_atom_dict()
    prm_df = _construct_df(mol_, lig_atoms, psf, prm, pairs14=False)
    prm_df14 = _construct_df(mol_, lig_atoms, psf, prm, pairs14=True)
    mol_.atoms = psf.to_atom_dict()

    # Convert Angstroem to bohr
    mol_ *= Units.conversion_ratio('angstrom', 'au')  # type: ignore[assignment]
    distance_upper_bound *= Units.conversion_ratio('angstrom', 'au')

    # The .prm format allows one to specify special non-bonded interactions between
    # atoms three bonds removed
    # If not specified, do not distinguish between atoms removed 3 and >3 bonds
    if prm_df14.isnull().values.all():
        prm_df14 = prm_df.copy()

    elif el_scale14 == lj_scale14 == 1:
        # Don't bother with a separate calculation for 1,4-nonbonded interactions
        return _get_V(prm_df, mol_, core_atoms,
                      shift_cutoff=shift_cutoff,
                      distance_upper_bound=distance_upper_bound,
                      depth_comparison=operator.__ge__)

    # Calculate the 1,4 - potential energies
    elstat14_df, lj14_df = _get_V(prm_df14, mol_, core_atoms,
                                  shift_cutoff=shift_cutoff,
                                  distance_upper_bound=distance_upper_bound,
                                  depth_comparison=operator.__eq__)
    elstat14_df *= el_scale14
    lj14_df *= lj_scale14

    # Calculate the total potential energies
    elstat_df, lj_df = _get_V(prm_df, mol_, core_atoms,
                              shift_cutoff=shift_cutoff,
                              distance_upper_bound=distance_upper_bound,
                              depth_comparison=operator.__gt__)
    elstat_df += elstat14_df
    lj_df += lj14_df

    return elstat_df, lj_df
コード例 #20
0
ファイル: jobs.py プロジェクト: pk-organics/CAT
def get_energy(self, index: int = -1, unit: str = 'Hartree') -> float:
    """Return the energy of the last occurence of ``'ENERGY| Total FORCE_EVAL'`` in the output."""
    energy_str = self.grep_output('ENERGY| Total FORCE_EVAL')[index]
    energy = float(energy_str.rsplit(maxsplit=1)[1])
    return Units.convert(energy, 'Hartree', unit)
コード例 #21
0
ファイル: ligand_opt.py プロジェクト: pk-organics/CAT
def set_dihed(self,
              angle: float,
              anchor: Atom,
              cap: Sequence[Atom],
              opt: bool = True,
              unit: str = 'degree') -> None:
    """Change all valid dihedral angles into a specific value.

    Performs an inplace update of this instance.

    Parameters
    ----------
    angle : :class:`float`
        The desired dihedral angle.

    anchor : |plams.Atom|
        The ligand anchor atom.

    opt : :class:`bool`
        Whether or not the dihedral adjustment should be followed up by an RDKit UFF optimization.

    unit : :class:`str`
        The input unit.

    """
    cap_atnum = []
    for at in cap:
        cap_atnum.append(at.atnum)
        at.atnum = 0

    angle = Units.convert(angle, unit, 'degree')
    bond_iter = (bond for bond in self.bonds
                 if bond.atom1.atnum != 1 and bond.atom2.atnum != 1
                 and bond.order == 1 and not self.in_ring(bond))

    # Correction factor for, most importantly, tri-valent anchors (e.g. P(R)(R)R)
    dihed_cor = angle / 2
    neighbors = anchor.neighbors()
    if len(neighbors) > 2:
        atom_list = [anchor] + sorted(neighbors, key=lambda at: -at.atnum)[:3]
        improper = get_dihed(atom_list)
        dihed_cor *= np.sign(improper)

    for bond in bond_iter:
        # Gather lists of all non-hydrogen neighbors
        n1, n2 = self.neighbors_mod(bond.atom1), self.neighbors_mod(bond.atom2)

        # Remove all atoms in `bond`
        n1 = [atom for atom in n1 if atom is not bond.atom2]
        n2 = [atom for atom in n2 if atom is not bond.atom1]

        # Remove all non-subsituted atoms
        # A special case consists of anchor atoms; they can stay
        if len(n1) > 1:
            n1 = [
                atom for atom in n1 if (len(self.neighbors_mod(atom)) > 1
                                        or atom is anchor or atom.atnum == 0)
            ]
        if len(n2) > 1:
            n2 = [
                atom for atom in n2 if (len(self.neighbors_mod(atom)) > 1
                                        or atom is anchor or atom.atnum == 0)
            ]

        # Set `bond` in an anti-periplanar conformation
        if n1 and n2:
            dihed = get_dihed((n1[0], bond.atom1, bond.atom2, n2[0]))
            if anchor not in bond:
                self.rotate_bond(bond,
                                 bond.atom1,
                                 angle - dihed,
                                 unit='degree')
            else:
                dihed -= dihed_cor
                self.rotate_bond(bond, bond.atom1, -dihed, unit='degree')
                dihed_cor *= -1

    for at, atnum in zip(cap, cap_atnum):
        at.atnum = atnum

    if opt:
        rdmol = molkit.to_rdmol(self)
        UFF(rdmol).Minimize()
        self.from_rdmol(rdmol)
コード例 #22
0
def parse_cp2k_value(param: Union[str, T],
                     unit: str,
                     default_unit: Optional[str] = None) -> T:
    """Parse and return the provided CP2K input parameter **param**.

    Examples
    --------
    Examples of valid input values for **param**.

    .. code:: python

        >>> param1: str = '[angstrom] 2.0'
        >>> param2: str = '2.5'
        >>> param3: float = 9.3
        >>> param4: int = 2
        ...


    Parameters
    ----------
    param : :class:`str`, :class:`float` or :class:`numpy.ndarray`
        The parameter.

    unit : :class:`str`
        The desired output unit of **param**.

    default_unit :class:`str`, optional
        The default input unit of **param** for when its unit is not explicitly specified.
        Will be ignored if ``None``.

    Returns
    -------
    :class:`float` or :class:`numpy.ndarray`
        The value **param** expressed in **unit**.

    Raises
    ------
    :exc:`ValueError`
        Raised if

    """
    default_unit = unit if default_unit is None else default_unit

    # Identify the unit and the quantity of interest
    if not isinstance(param, str):
        value_unit = None
    else:
        value_unit, _param = param.split()
        param = float(_param)

    if not value_unit:
        return param * Units.conversion_ratio(default_unit, unit)

    # Correct the units
    value_unit_lower = value_unit.lower().strip('[').rstrip(']')
    try:  # Convert from CP2K to PLAMS Units
        value_unit_parsed = UNIT_MAP[value_unit_lower]
    except KeyError as ex:
        raise ValueError(
            f"Invalid unit {value_unit_lower!r};\naccepted units: "
            f"{tuple(UNIT_MAP.keys())!r}") from ex
    return param * Units.conversion_ratio(value_unit_parsed, unit)
コード例 #23
0
def get_bonded(
    mol: str | bytes | os.PathLike[Any] | MultiMolecule,
    psf: str | bytes | os.PathLike[Any] | PSFContainer,
    prm: str | bytes | os.PathLike[Any] | PRMContainer,
) -> tuple[None | pd.DataFrame, None | pd.DataFrame, None | pd.DataFrame, None
           | pd.DataFrame, None | pd.DataFrame, ]:
    r"""Collect forcefield parameters and calculate all intra-ligand interactions in **mol**.

    Forcefield parameters are collected from the provided **psf** and **prm** files.

    Parameters
    ----------
    mol : :class:`str` or :class:`MultiMolecule`
        A MultiMolecule instance or the path+filename of an .xyz file.

    psf : :class:`str` or :class:`PSFContainer`
        A PSFContainer instance or the path+filename of a .psf file.
         Used for setting :math:`q` and creating atom-subsets.

    prm : :class:`str` or :class:`PRMContainer`, optional
        A PRMContainer instance or the path+filename of a .prm file.
        Used for setting :math:`\sigma` and :math:`\varepsilon`.

    Returns
    -------
    5x :class:`pandas.DataFrame` and/or ``None``
        Four series with the potential energies of all bonds, angles, Urey-Bradley terms,
        proper and improper dihedral angles.
        A DataFrame is replaced with ``None`` if no parameters are available for that particular
        section.
        Units are in atomic units.

    """
    # Read the .psf file and switch from 1- to 0-based atomic indices
    if not isinstance(psf, PSFContainer):
        psf_ = PSFContainer.read(psf)
    else:
        psf_ = psf.copy()
    psf_.bonds -= 1
    psf_.angles -= 1
    psf_.dihedrals -= 1
    psf_.impropers -= 1

    # Read the molecule
    if not isinstance(mol, MultiMolecule):
        mol = MultiMolecule.from_xyz(mol)
    else:
        mol = mol.copy(deep=False)
    mol.atoms = psf_.to_atom_dict()
    symbols = sorted(mol.atoms.keys())

    # Extract parameters from the .prm file
    bonds, angles, urey_bradley, dihedrals, impropers = process_prm(prm)

    kcal2au = Units.conversion_ratio('kcal/mol', 'au')

    # Calculate the various potential energies
    if bonds is not None:
        parse_wildcards(bonds, symbols, prm_type='bonds')
        bonds_ret = get_V_bonds(bonds, mol, psf_.bonds)
        bonds_ret *= kcal2au
    else:
        bonds_ret = None

    if angles is not None:
        parse_wildcards(angles, symbols, prm_type='angles')
        angles_ret = get_V_angles(angles, mol, psf_.angles)
        angles_ret *= kcal2au
    else:
        angles_ret = None

    if urey_bradley is not None:
        parse_wildcards(urey_bradley, symbols, prm_type='urey_bradley')
        urey_bradley_ret = get_V_UB(urey_bradley, mol, psf_.angles)
        urey_bradley_ret *= kcal2au
    else:
        urey_bradley_ret = None

    if dihedrals is not None:
        parse_wildcards(dihedrals, symbols, prm_type='dihedrals')
        dihedrals_ret = get_V_dihedrals(dihedrals, mol, psf_.dihedrals)
        dihedrals_ret *= kcal2au
    else:
        dihedrals_ret = None

    if impropers is not None:
        parse_wildcards(impropers, symbols, prm_type='impropers')
        impropers_ret = get_V_impropers(impropers, mol, psf_.impropers)
        impropers_ret *= kcal2au
    else:
        impropers_ret = None

    return bonds_ret, angles_ret, urey_bradley_ret, dihedrals_ret, impropers_ret
コード例 #24
0
    try:
        mol.properties.job_path += [
            join(job.path, job.name + '.in') for job in job_list
        ]
    except IndexError:  # The 'job_path' key is not available
        mol.properties.job_path = [
            join(job.path, job.name + '.in') for job in job_list
        ]

    ret = [E_solv[k] for k in ("acid", "base", "solvent", "solvent_conj")]
    ret.append(_get_pka(**E_solv))
    return ret


#: The gas constant in kcal/mol
R: float = Units.convert(constants.R, 'kj/mol', 'kcal/mol') / 1000


def _get_pka(acid: float,
             base: float,
             solvent: float,
             solvent_conj: float,
             T: float = 298.15) -> float:
    """Calculate the pKa at the temperature **T**.

    See Also
    --------
    `Molecular Physics 108, 229 (2010) <https://doi.org/10.1080/00268970903313667>`_
        F. Eckert, M. Diedenhofen, and A. Klamt, Towards a first principles prediction of
        pKa : COSMO-RS and the cluster-continuum approach.
コード例 #25
0
def get_asa_md(mol_list: Iterable[Molecule],
               jobs: Tuple[Type[Job], ...],
               settings: Tuple[Settings, ...],
               iter_start: int = 500,
               el_scale14: float = 0.0,
               lj_scale14: float = 1.0,
               distance_upper_bound: float = np.inf,
               k: int = 20,
               dump_csv: bool = False,
               **kwargs: Any) -> np.ndarray:
    r"""Perform an activation strain analyses (ASA) along an molecular dynamics (MD) trajectory.

    The ASA calculates the (ensemble-averaged) interaction, strain and total energy.

    Parameters
    ----------
    mol_list : :class:`Iterable<collectionc.abc.Iterable>` [:class:`Molecule`]
        An iterable consisting of PLAMS molecules.

    jobs : :class:`tuple` [|plams.Job|]
        A tuple containing a single |plams.Job| type.

    settings : :class:`tuple` [|plams.Settings|]
        A tuple containing a single |plams.Settings| instance.

    iter_start : :class:`int`
        The MD iteration at which the ASA will be started.
        All preceding iteration are disgarded, treated as pre-equilibration steps.

    el_scale14 : :class:`float`
        Scaling factor to apply to all 1,4-nonbonded electrostatic interactions.
        Serves the same purpose as the cp2k ``EI_SCALE14`` keyword.

    lj_scale14 : :class:`float`
        Scaling factor to apply to all 1,4-nonbonded Lennard-Jones interactions.
        Serves the same purpose as the cp2k ``VDW_SCALE14`` keyword.

    distance_upper_bound : :class:`float`
        Consider only atom-pairs within this distance for calculating inter-ligand interactions.
        Units are in Angstrom.
        Using ``inf`` will default to the full, untruncated, distance matrix.

    k : :class:`int`
        The (maximum) number of to-be considered distances per atom.
        Only relevant when **distance_upper_bound** is not set to ``inf``.

    dump_csv : :class:`str`, optional
        If ``True``, dump the raw energy terms to a set of .csv files.

    \**kwargs : :data:`Any<typing.Any>`
        Further keyword arguments for ensuring signature compatiblity.

    Returns
    -------
    :math:`n*3` |np.ndarray|_ [|np.float64|_]
        Returns a 2D array respectively containing :math:`E_{int}`, :math:`E_{strain}`
        and :math:`E`.
        Ensemble-averaged energies are calculated for the, to-be computed, MD trajectories of
        all *n* molecules in **mol_list**.

    """
    # Extract all Job types and job Settings
    job = jobs[0]
    s = settings[0].copy()
    if job is not Cp2kJob:
        raise ValueError(
            "'jobs' expected '(Cp2kJob,)'; observed value: {repr(jobs)}")

    # Infer the shape of the to-be created energy array
    try:
        mol_len = len(mol_list)
    except TypeError:  # **mol_list*** is an iterator
        shape = -1, 5
        count = -1
    else:
        shape = mol_len, 5
        count = mol_len * 5

    # Extract all energies and ligand counts
    iterator = chain.from_iterable(
        md_generator(mol_list,
                     job,
                     s,
                     iter_start=iter_start,
                     el_scale14=el_scale14,
                     lj_scale14=lj_scale14,
                     distance_upper_bound=distance_upper_bound,
                     k=k,
                     dump_csv=dump_csv))

    E = np.fromiter(iterator, count=count, dtype=float)
    E.shape = shape
    E[:, :4] *= Units.conversion_ratio('hartree', 'kcal/mol')

    # Calculate (and return) the interaction, strain and total energy
    E_int = E[:, 0]
    E_strain = E[:, 1:3].sum(axis=1) - E[:, 3:].prod(axis=1)
    return np.array([E_int, E_strain, E_int + E_strain]).T