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
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
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
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)
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()
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 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
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
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
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])