def get_core_angle(core: Molecule) -> Tuple[float, float]: """Return the mean.""" # Find all nearest anchor neighbours anchors = np.array([at.coords for at in core.properties.dummies]) dist = cdist(anchors, anchors) np.fill_diagonal(dist, 10000) idx = np.argmin(dist, axis=0) # Construct (and normalize) vectors from the center of mass to the anchor atoms center = np.array(core.get_center_of_mass()) vec1 = anchors - center vec2 = anchors[idx] - center vec1 /= np.linalg.norm(vec1, axis=1)[..., None] vec2 /= np.linalg.norm(vec2, axis=1)[..., None] # Calculate (and average) all the anchor-center-anchor angles r_ref = np.linalg.norm(anchors - anchors[idx], axis=1) dot = np.einsum('ij,ij->i', vec1, vec2) return np.arccos(dot).mean(), r_ref.mean() # Theta and d
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 _construct_xyn( ion: str | int | Molecule, lig_count: int, lig: Molecule, lig_at: Atom, lig_idx: int, ) -> Tuple[Molecule, Atom]: """Construct the :math:`XYn` molecule for :func:`get_xyn`. Parameters ---------- ion : |str|_, |int|_ or |plams.Molecule|_ An ion (:math:`X`), be it mono- (*e.g.* atomic number or symbol) or poly-atomic. lig_count : int The number of to-be attached ligands per ion. lig : |plams.Molecule|_ A single ligand molecule. lig_at : |plams.Atom|_ The ligand anchor atom. lig_idx : int The (1-based) index of **lig_at**. Returns ------- |plams.Molecule|_ and |plams.Atom|_ A :math:`XY_{n}` molecule and the the charged atom from :math:`X`. """ # Create a list of n ligands, n anchor atoms, n desired ion-anchor distances and n angles lig_gen = (lig.copy() for _ in range(lig_count)) angle_ar = np.arange(0, 2 * np.pi, 2 * np.pi / lig_count) # Prepare vectors for translations and rotations vec1 = lig_at.vector_to(np.zeros(3)) _vec = lig_at.vector_to(lig.get_center_of_mass()) vec2 = get_perpendicular_vec(_vec) # Update the XYn molecule with ligands XYn, X = _parse_ion(ion) iterator = enumerate(zip(angle_ar, lig_gen), 2) for i, (angle, mol) in iterator: # Prepare for translations and rotations anchor = mol[lig_idx] rotmat = axis_rotation_matrix(vec2, angle) dist = anchor.radius + X.radius # Translate and rotate the ligand mol.translate(vec1) mol.rotate(rotmat) vec3 = anchor.vector_to(mol.get_center_of_mass()) vec3 /= np.linalg.norm(vec3) / dist mol.translate(vec3) # Set pdb attributes for at in mol: at.properties.pdb_info.ResidueNumber = i at.properties.pdb_info.ResidueName = 'LIG' # Combine the translated and rotated ligand with XYn XYn.add_molecule(mol) XYn.add_bond(X, anchor) return XYn, X