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)
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)
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
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)
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
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
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
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
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)')
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
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))
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)
#!/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:
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)
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
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
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
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')
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
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)
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)
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)
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
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.
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