Esempio n. 1
0
def sanitize_dim_3(value: Any, padding: float = np.nan) -> np.ndarray:
    """Convert a Molecule or sequence of :math:`m` molecules into an :math:`m*n*3` array.

    If necessary, the to-be returned array is padded with **padding** .

    Parameters
    ----------
    arg : object
        The to be parsed object.
        Acceptable types are:

        * A PLAMS :class:`Atom`
        * A PLAMS :class:`Molecule`
        * A (nested) Sequences consisting of PLAMS :class:`Atom`
        * A (nested) Sequences consisting of PLAMS :class:`Molecule`
        * An array-like object with a dimensionality smaller than 3 and float-compatible elements.

    padding : float
        A value used for padding the to-be returned array.
        Only relevant if **arg** consists of multiple molecule with different numbers of atoms.

    Returns
    -------
    :math:`m*n*3` |np.ndarray|_ [|np.float64|_]
        A 3D array consisting of floats.

    Raises
    ------
    ValueError
        Raised if dimensionality of the to-be returned array is higher than 3
        or the content of **arg** cannot be converted into an array of floats.

    """
    if _is_atom(value):
        return np.array(value.coords)[None, None, :]

    elif _is_atom_sequence(value):
        return Molecule.as_array(None, atom_subset=value)[None, :]

    elif _is_mol_sequence(value):
        max_at = max(len(mol) for mol in value)
        ret = np.full((len(value), max_at, 3), padding, order='F')
        for i, mol in enumerate(value):
            j = len(mol)
            ret[i, :j] = Molecule.as_array(None, atom_subset=mol)
        return ret

    else:
        ret = np.array(value, ndmin=3, dtype=float, copy=False)
        if ret.ndim > 3:
            raise ValueError(f"Failed to create a 3D array; observed dimensionality: {ret.ndim}")
        return ret
Esempio n. 2
0
def _multi_lig_anchor(qd_series, ligands, path, anchor,
                      allignment) -> np.ndarray:
    """Gogogo."""
    ret = np.empty((len(ligands), len(qd_series)), dtype=object)
    for i, qd in enumerate(qd_series):
        qd = qd.copy()

        for j, (ligand, atnum) in enumerate(zip(ligands, anchor)):
            qd.set_atoms_id()
            try:
                atoms = [at for at in qd if at.atnum == atnum]
                assert atoms
            except AssertionError as ex:
                raise MoleculeError(
                    f'Failed to identify {to_symbol(atnum)!r} in '
                    f'{qd.get_formula()!r}') from ex

            coords = Molecule.as_array(None, atom_subset=atoms)
            qd.properties.dummies = np.array(coords, ndmin=2, dtype=float)
            qd = ligand_to_qd(qd,
                              ligand,
                              path=path,
                              allignment=allignment,
                              idx_subset=qd.properties.indices)
            ret[j, i] = qd
            for at in reversed(atoms):
                qd.delete_atom(qd[at.id])
            qd.unset_atoms_id()
    return ret
Esempio n. 3
0
def _get_xyz(mol: Molecule, atom: AtomSymbol) -> np.ndarray:
    """Return the Cartesian coordinates of **mol** belonging to the atom subset of *atom*."""
    atnum = to_atnum(atom)
    xyz = mol.as_array(atom_subset=(at for at in mol if at.atnum == atnum))

    if not xyz.any():
        raise MoleculeError(f"No atoms with atomic symbol {to_symbol(atom)!r} "
                            f"in {mol.get_formula()!r}")
    return xyz
Esempio n. 4
0
def allign_axis(mol: Molecule, anchor: Atom):
    """Allign a molecule with the Cartesian X-axis; setting **anchor** as the origin."""
    try:
        idx = mol.atoms.index(anchor)
    except ValueError as ex:
        raise MoleculeError("The passed anchor is not in mol") from ex

    xyz = mol.as_array()  # Allign the molecule with the X-axis
    rotmat = optimize_rotmat(xyz, idx)
    xyz[:] = xyz @ rotmat.T
    xyz -= xyz[idx]
    xyz[:] = xyz.round(decimals=3)
    mol.from_array(xyz)
Esempio n. 5
0
def label_core(mol):
    """Adds plams_mol.properties attribute to the core.

    Reads the atom indices from comment section in core's .xyz file and adds
    additional plams_mol.properties: coordinates of atom that will be substituted,
    bond vector between the substitution atom and its connection at the core,
    coordinates of the connection at the core

    Parameters
    ----------
    mol : |plams.Molecule|
        An input  PLAMS molecule with atom indices to be substituted in
        plams_mol.properties.comment

    Returns
    -------
    mol : |plams.Molecule|
        A PLAMS mol with additional plams_mol.properties

    """
    # Read the comment in the second line of the xyz file
    comment = mol.properties.comment
    comment = comment.split()
    mol.properties.core_len = len(mol)

    idx = np.array(comment, dtype=int)
    at_h = [mol[int(i)] for i in idx]
    at_other = [at.bonds[0].other_end(at) for at in at_h]

    dummy = Molecule()
    mol.properties.coords_h = dummy.as_array(atom_subset=at_h)
    mol.properties.coords_other = dummy.as_array(atom_subset=at_other)
    mol.properties.vec = mol.properties.coords_other - mol.properties.coords_h

    mol.properties.coords_h_atom = at_h
    mol.properties.coords_other_atom = at_other
    mol.properties.coords_other_arrays = [np.array(i.coords) for i in at_other]
    mol.guess_bonds()
Esempio n. 6
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))
Esempio n. 7
0
def sanitize_dim_2(value: Any) -> np.ndarray:
    """Convert a PLAMS atom or sequence of :math:`n` PLAMS atoms into a :math:`n*3` array.

    Parameters
    ----------
    value : object
        The to be parsed object.
        Acceptable types are:

        * A PLAMS :class:`Atom`.
        * A PLAMS :class:`Molecule`.
        * A Sequence consisting of PLAMS :class:`Atom`.
        * An array-like object with a dimensionality smaller than 2 and float-compatible elements.

    Returns
    -------
    :math:`n*3` |np.ndarray|_ [|np.float64|_]
        A 2D array consisting of floats.

    Raises
    ------
    ValueError
        Raised if dimensionality of the to-be returned array is higher than 2
        or the content of **arg** cannot be converted into an array of floats.

    """
    value = value.coords if isinstance(value, Atom) else value
    try:
        ret = np.array(value, dtype=float, copy=False)
    except TypeError:
        ret = Molecule.as_array(None, atom_subset=value)

    if ret.ndim < 2:
        ret.shape = (2 - ret.ndim) * (1,) + ret.shape
    elif ret.ndim > 2:
        raise ValueError(f"Failed to create a 2D array; observed dimensionality: {ret.ndim}")
    return ret
Esempio n. 8
0
def ligand_to_qd(core: Molecule, ligand: Molecule, path: str,
                 allignment: str = 'sphere',
                 idx_subset: Optional[Iterable[int]] = None) -> Molecule:
    """Function that handles quantum dot (qd, *i.e.* core + all ligands) operations.

    Combine the core and ligands and assign properties to the quantom dot.

    Parameters
    ----------
    core : |plams.Molecule|_
        A core molecule.

    ligand : |plams.Molecule|_
        A ligand molecule.

    allignment : :class:`str`
        How the core vector(s) should be defined.
        Accepted values are ``"sphere"`` and ``"surface"``:

        * ``"sphere"``: Vectors from the core anchor atoms to the center of the core.
        * ``"surface"``: Vectors perpendicular to the surface of the core.

        Note that for a perfect sphere both approaches are equivalent.

    idx_subset : :class:`Iterable<collections.anc.Iterable>` [:class:`int`], optional
        An iterable with the (0-based) indices defining a subset of atoms in **core**.
        Only relevant in the construction of the convex hull when ``allignment=surface``.

    Returns
    -------
    |plams.Molecule|_
        A quantum dot consisting of a core molecule and *n* ligands

    """
    def get_name() -> str:
        core_name = core.properties.name
        anchor = str(qd[-1].properties.pdb_info.ResidueNumber - 1)
        lig_name = ligand.properties.name
        return f'{core_name}__{anchor}_{lig_name}'

    idx_subset_ = idx_subset if idx_subset is not None else ...

    # Define vectors and indices used for rotation and translation the ligands
    vec1 = np.array([-1, 0, 0], dtype=float)  # All ligands are already alligned along the X-axis
    idx = ligand.get_index(ligand.properties.dummies) - 1
    ligand.properties.dummies.properties.anchor = True

    # Attach the rotated ligands to the core, returning the resulting strucutre (PLAMS Molecule).
    if allignment == 'sphere':
        vec2 = np.array(core.get_center_of_mass()) - sanitize_dim_2(core.properties.dummies)
        vec2 /= np.linalg.norm(vec2, axis=1)[..., None]
    elif allignment == 'surface':
        if isinstance(core.properties.dummies, np.ndarray):
            anchor = core.properties.dummies
        else:
            anchor = core.as_array(core.properties.dummies)
        vec2 = -get_surface_vec(np.array(core)[idx_subset_], anchor)
    else:
        raise ValueError(repr(allignment))

    lig_array = rot_mol(ligand, vec1, vec2, atoms_other=core.properties.dummies, core=core, idx=idx)
    qd = core.copy()
    array_to_qd(ligand, lig_array, mol_out=qd)
    qd.round_coords()

    # Set properties
    qd.properties = Settings({
        'indices': [i for i, at in enumerate(qd, 1) if
                    at.properties.pdb_info.ResidueName == 'COR' or at.properties.anchor],
        'path': path,
        'name': get_name(),
        'job_path': [],
        'prm': ligand.properties.get('prm')
    })

    # Print and return
    _evaluate_distance(qd, qd.properties.name)
    return qd
Esempio n. 9
0
 def __enter__(self) -> np.ndarray:
     """Enter the context manager; return an array of Cartesian coordinates."""
     self._xyz = Molecule.as_array(None, atom_subset=self.mol)
     return self._xyz
Esempio n. 10
0
def get_entropy(mol: Molecule,
                freqs: np.ndarray,
                T: float = 298.15) -> np.ndarray:
    """Calculate the translational, vibrational and rotational entropy.

    All units and constants are in SI units.

    Parameters
    ----------
    mol : |plams.Molecule|_
        A PLAMS molecule.

    freqs : |np.ndarray|_ [|np.float64|_]
        An iterable consisting of vibrational frequencies in units of cm**-1.

    T : float
        The temperature in Kelvin.

    Returns
    -------
    |np.ndarray|_ [|np.float64|_]:
        An array with translational, rotational and vibrational contributions to the entropy,
        ordered in that specific manner.
        Units are in J/mol.

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

    # Extract atomic masses and Cartesian coordinates
    m = 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(m * (y**2 + z**2)), -sum(m * x * y), -sum(m * x * z)],
         [-sum(m * x * y),
          sum(m * (x**2 + z**2)), -sum(m * y * z)],
         [-sum(m * x * z), -sum(m * y * z),
          sum(m * (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, rotational and vibrational entropy (divided by R)
    S_trans = 1.5 + np.log(V_Na * ((2 * pi * sum(m) * kT) / h**2)**1.5)
    S_rot = 1.5 + np.log(q_rot)
    with np.errstate(divide='ignore', invalid='ignore'):
        S_vib_left = hv_kT / np.expm1(hv_kT)
        S_vib_left[np.isnan(S_vib_left)] = 0.0
        S_vib_right = np.log(1 - np.exp(-hv_kT))
        S_vib_right[S_vib_right == -np.inf] = 0.0
    S_vib = sum(S_vib_left - S_vib_right)

    return R * np.array([S_trans, S_rot, S_vib])