def cap_fragments(self, bond: Bond) -> Dict[Atom, Atom]: """Delete a bond from :attr:`SplitMol.mol` and cap the resulting fragments with :attr:`SplitMol.cap_type`. Parameters ---------- bond : |plams.Bond| A PLAMS bond. Returns ------- :class:`dict` [|plams.Atom|, |plams.Atom|] A dictionary with the old atoms in **bond** as keys and their new capping atoms as values. """ # noqa mol = self.mol symbol = self.cap_type # Construct the capping atoms atom1, atom2 = bond.atom1, bond.atom2 atom1_cap = Atom(symbol=symbol, coords=atom2.coords) atom2_cap = Atom(symbol=symbol, coords=atom1.coords) mol.add_atom(atom1_cap, adjacent=[atom1]) mol.add_atom(atom2_cap, adjacent=[atom2]) # Resize the capping atom bond length1 = atom1.radius + atom1_cap.radius length2 = atom2.radius + atom2_cap.radius mol.bonds[-2].resize(atom1_cap, length1) mol.bonds[-1].resize(atom2_cap, length2) # Delete the old bond and return a dictionary containg marking all new bonds return {atom1: atom1_cap, atom2: atom2_cap}
def dfs(at1: Atom, atom_set: Set[Atom]): at1._visited = True atom_set.add(at1) for at2 in at1.neighbors(): if at2._visited: continue dfs(at2, atom_set)
def string_array_to_molecule(parser_fun: ParserElement, file_name: PathLike, mol: Optional_[Molecule] = None) -> Molecule: """Convert a Numpy string array. It takes an array like: [['C', '-1.487460', '-0.028670', '-0.000060'], ['O', '0.376340', '0.028670', '-0.000060'], ['H', '-1.818910', '-1.067060', '-0.000060'], ['H', '-1.866470', '0.473700', '0.889930'], ['H', '-1.866470', '0.473700', '-0.890040'], ['H', '0.756720', '-0.950010', '-0.000060']] and covert it to a plams ``Molecule``. """ mols = parse_file(parser_fun, file_name).asList() last_mol = np.array(mols[-1]) elems = last_mol[:, 0] coords = np.array(last_mol[:, 1:], dtype=float) if mol: if len(coords) == len(mol): mol.from_array(coords) else: raise RuntimeError('Output molecule does not match input molecule') else: mol = Molecule() for e, c in zip(elems, coords): mol.add_atom(Atom(symbol=e, coords=tuple(c))) return mol
def string_array_to_molecule(parser_fun, file_name, mol=None): """ Convert a Numpy string array like: [['C', '-1.487460', '-0.028670', '-0.000060'], ['O', '0.376340', '0.028670', '-0.000060'], ['H', '-1.818910', '-1.067060', '-0.000060'], ['H', '-1.866470', '0.473700', '0.889930'], ['H', '-1.866470', '0.473700', '-0.890040'], ['H', '0.756720', '-0.950010', '-0.000060']] To a plams ``Molecule``. """ string_array_to_float = np.vectorize(float) mols = parse_file(parser_fun, file_name).asList() last_mol = np.array(mols[-1]) elems = last_mol[:, 0] coords = string_array_to_float(last_mol[:, 1:]) if mol: if len(coords) == len(mol): plams_mol = mol for i in range(len(plams_mol)): plams_mol.atoms[i].coords = tuple( [float(c) for c in coords[i]]) else: raise RuntimeError('Output molecule does not match input molecule') else: plams_mol = Molecule() for e, c in zip(elems, coords): plams_mol.add_atom(Atom(symbol=e, coords=tuple(c))) return plams_mol
def dfs(at1: Atom, m_append: Callable[[int], None]): at1._visited = True # type: ignore[attr-defined] m_append(at1.id) for bond in at1.bonds: at2 = bond.other_end(at1) if not at2._visited: # type: ignore[attr-defined] dfs(at2, m_append)
def repr_Atom(self, obj: Atom, level: int) -> str: # noqa: N802 """Create a :class:`str` representation of a |plams.Atom| instance.""" decimal = self.maxfloat space = 14 - ( 6 - decimal ) # The default PLAMS values for space and decimal are 14 and 6 ret = obj.str(decimal=decimal, space=space).strip() return f'{obj.__class__.__name__}({ret})'
def tuplesXYZ_to_plams(xs): """ Transform a list of namedTuples to a Plams molecule """ plams_mol = Molecule() for at in xs: symb = at.symbol cs = at.xyz plams_mol.add_atom(Atom(symbol=symb, coords=tuple(cs))) return plams_mol
def geometry_to_molecule(geometry): """ Convert a list of XYZ coordinates to a Molecule object """ mol = Molecule() for i in range(0, len(geometry)): mol.add_atom( Atom(symbol=geometry[i][0], coords=(geometry[i][1], geometry[i][2], geometry[i][3]))) return mol
def parse_molecule_traj(file_traj: PathLike) -> Molecule: """Read Molecules from the job_name.traj file.""" mols = manyXYZ(file_traj) # Last geometry corresponds to the optimized structure opt_mol = mols[-1] plams_mol = Molecule() for at in opt_mol: symb = at.symbol cs = at.xyz plams_mol.add_atom(Atom(symbol=symb, coords=tuple(cs))) return plams_mol
def _test_distribute(mol: Molecule, symbol: str, **kwargs) -> Molecule: """Helper function for :func:`test_distribute`.""" if not isinstance(mol, Molecule): mol = Molecule(mol) _idx_in = [i for i, at in enumerate(mol) if at.symbol == symbol] idx_in = np.fromiter(_idx_in, count=len(_idx_in), dtype=int) idx_out = distribute_idx(mol, idx_in, **kwargs) a = symbol b = 'I' if a != 'I' else 'Br' mol2 = Molecule() for i, at in enumerate(mol): if at.symbol != symbol: continue symbol_new = a if i not in idx_out else b mol2.add_atom(Atom(symbol=symbol_new, coords=at.coords, mol=mol2)) return mol2
def _parse_ion(ion: Molecule | str | int) -> Tuple[Molecule, Atom]: """Interpret and parse the **ion** argument in :func:`.get_xyn`. Construct and return a new :math:`XY_{n=0}` molecule and the atom :math:`X` itself. If **ion** is a polyatomic ion then :math:`XY_{n=0}` is a copy of **ion** and :math:`X` is the first atom with a non-zero charge. Parameters ---------- ion : |str|_, |int|_ or |plams.Molecule|_ An ion (:math:`X`), be it mono- (*e.g.* atomic number or symbol) or poly-atomic. Returns ------- |plams.Molecule|_ and |plams.Atom|_ A :math:`XY_{n=0}` molecule and the the charged atom from :math:`X`. Raises ------ MoleculeError Raised if ion is an instance of :math:`Molecule` but does not contain any charged atoms. """ if isinstance(ion, Molecule): XYn = ion.copy() for i, at in enumerate(XYn, 1): if not at.properties.charge: continue # Found an atom with non-zero charge; return a copy ret = XYn.copy() return ret, ret[i] raise MoleculeError( "No atoms were found in 'ion' with a non-zero charge") else: # Ion is an atomic number or symbol X = Atom(atnum=to_atnum(ion)) XYn = Molecule() XYn.add_atom(X) return XYn, X
def _molecule_from_rdmol( rdmol: Chem.Mol, smiles: str, matches: Iterable[Sequence[int]], split: bool = True, ) -> Generator[Molecule, None, None]: """Construct a PLAMS molecule from the passed rdkit mol's ``MolBlock``.""" for tup in matches: try: i, *_, j = tup # type: int, Any, None | int except ValueError: i = tup[0] j = None # Split the capping atom (j) from the main molecule if j is not None and split: if i > j: i -= 1 rdmol_edit = Chem.EditableMol(rdmol) rdmol_edit.RemoveAtom(j) rdmol_new = rdmol_edit.GetMol() anchor = rdmol_new.GetAtoms()[i] anchor.SetFormalCharge(anchor.GetFormalCharge() - 1) else: rdmol_new = rdmol # Parse the .mol block and convert it into a PLAMS molecule mol_block = Chem.MolToMolBlock(rdmol) iterator = _iter_mol_block(mol_block, size=len(rdmol.GetAtoms())) mol = Molecule() mol.atoms = [Atom(symbol=symbol, coords=xyz, mol=mol) for symbol, xyz in iterator] for bond in rdmol.GetBonds(): at1 = mol.atoms[bond.GetBeginAtomIdx()] at2 = mol.atoms[bond.GetEndAtomIdx()] mol.add_bond(Bond(at1, at2, order=bond.GetBondTypeAsDouble())) # Set properties and yield mol.properties.smiles = smiles mol.properties.dummies = mol.atoms[i] mol.properties.anchor = f"{mol.properties.dummies.symbol}{i + 1}" yield mol
def from_rdmol(rdkit_mol, confid=-1): """ Translate an RDKit molecule into a PLAMS molecule type. :parameter rdkit_mol: RDKit molecule :parameter int confid: conformer identifier from which to take coordinates :type rdkit_mol: rdkit.Chem.Mol :return: a PLAMS molecule :rtype: plams.Molecule """ if isinstance(rdkit_mol, Molecule): return rdkit_mol # Create plams molecule plams_mol = Molecule() total_charge = 0 try: Chem.Kekulize(rdkit_mol) except: pass conf = rdkit_mol.GetConformer(id=confid) for rd_atom in rdkit_mol.GetAtoms(): pos = conf.GetAtomPosition(rd_atom.GetIdx()) ch = rd_atom.GetFormalCharge() pl_atom = Atom(rd_atom.GetAtomicNum(), coords=(pos.x, pos.y, pos.z), charge=ch) if rd_atom.GetPDBResidueInfo(): pl_atom.properties.pdb_info = get_PDBResidueInfo(rd_atom) plams_mol.add_atom(pl_atom) total_charge += ch for bond in rdkit_mol.GetBonds(): at1 = plams_mol.atoms[bond.GetBeginAtomIdx()] at2 = plams_mol.atoms[bond.GetEndAtomIdx()] plams_mol.add_bond(Bond(at1, at2, bond.GetBondTypeAsDouble())) plams_mol.charge = total_charge for propname in rdkit_mol.GetPropNames(): plams_mol.properties[propname] = rdkit_mol.GetProp(propname) return plams_mol
def test_index(): """Test :meth:`Molecule.index`.""" atom = BENZENE[1] bond = BENZENE[1, 2] atom_test = Atom(coords=[0, 0, 0], symbol='H') assert BENZENE.index(atom) == 1 assert BENZENE.index(bond) == (1, 2) try: BENZENE.index(None) # None is of invalid type except MoleculeError: pass else: raise AssertionError( "'BENZENE.index(None)' failed to raise a 'MoleculeError'") try: BENZENE.index(atom_test) # atom_test is not in BENZENE except MoleculeError: pass else: raise AssertionError( "'BENZENE.index(atom_test)' failed to raise a 'MoleculeError'")
def run_ff_anionic(mol: Molecule, anchor: Atom, s: Settings) -> None: r"""Assign neutral parameters to an anionic species (*e.g.* carboxylate). Consists of 4 distinct steps: * **mol** is capped with a proton: *e.g.* :math:`RCO_2^- \rightarrow RCO_2H`. * Parameters are guessed for both fragments (using MATCH_) and then recombined into **mol**. * The capping proton is removed again. * The atomic charge of **anchor** is adjusted such that the total moleculair charge becomes zero. Performs an inplace update of **mol**. .. _MATCH: http://brooks.chem.lsa.umich.edu/index.php?page=match&subdir=articles/resources/software Parameters ---------- mol : :class:`Molecule<scm.plams.mol.molecule.Molecule>` A cationic molecule. anchor : :class:`Atom<scm.plams.mol.atom.Atom>` The atom in **mol** with the formal negative charge. s : :class:`Settings<scm.plams.core.settings.Settings>` The job Settings to-be passed to :class:`MATCHJob<nanoCAT.ff.match_job.MATCHJob>`. See Also -------- :func:`run_match_job()<nanoCAT.ff.ff_assignment.run_match_job>` Assign atom types and charges to **mol** based on the results of MATCH_. :func:`run_ff_cationic()<nanoCAT.ff.ff_cationic.run_ff_cationic>` Assign neutral parameters to a cationic species (*e.g.* ammonium). """ # noqa if anchor not in mol: raise MoleculeError("Passed 'anchor' is not part of 'mol'") anchor.properties.charge = 0 # Cap the anion with a proton mol_with_h = add_Hs(mol) _cap_h = mol_with_h[-1] cap_h = Atom(atnum=_cap_h.atnum, coords=_cap_h.coords, mol=mol, settings=mol[1].properties.copy()) cap_h.properties.pdb_info.IsHeteroAtom = False cap_h.properties.pdb_info.Name = 'Hxx' mol.add_atom(cap_h) mol.add_bond(Bond(anchor, cap_h, mol=mol)) # Guess parameters and remove the capping proton run_match_job(mol, s) mol.delete_atom(cap_h) # Set the total charge of the system to 0 anchor.properties.charge_float -= sum(at.properties.charge_float for at in mol) return None
def run_ff_cationic(mol: Molecule, anchor: Atom, s: Settings) -> None: r"""Assign neutral parameters to a cationic species (*e.g.* ammonium). Consists of 3 distinct steps: * **mol** is converted into two neutral fragments, *e.g.* ammonium is converted into two amines: :math:`N^+(R)_4 \rightarrow N(R)_3 + RN(H)_2`. * Parameters are guessed for both fragments (using MATCH_) and then recombined into **mol**. * The atomic charge of **anchor** is adjusted such that the total moleculair charge becomes zero. Performs an inplace update of **mol**. .. _MATCH: http://brooks.chem.lsa.umich.edu/index.php?page=match&subdir=articles/resources/software Parameters ---------- mol : :class:`Molecule<scm.plams.mol.molecule.Molecule>` A cationic molecule. anchor : :class:`Atom<scm.plams.mol.atom.Atom>` The atom in **mol** with the formal positive charge. s : :class:`Settings<scm.plams.core.settings.Settings>` The job Settings to-be passed to :class:`MATCHJob<nanoCAT.ff.match_job.MATCHJob>`. See Also -------- :func:`run_match_job()<nanoCAT.ff.ff_assignment.run_match_job>` Assign atom types and charges to **mol** based on the results of MATCH_. :func:`run_ff_anionic()<nanoCAT.ff.ff_anionic.run_ff_anionic>` Assign neutral parameters to an anionic species (*e.g.* carboxylate). """ # noqa if anchor not in mol: raise MoleculeError("Passed 'anchor' is not part of 'mol'") anchor.properties.charge = 0 # Find the first bond attached to the anchor atom which is not part of a ring for bond in anchor.bonds: if not mol.in_ring(bond): break else: raise MoleculeError( "All bonds attached to 'anchor' are part of a ring system") with SplitMol(mol, bond) as (frag1, frag2): # Identify the amine and the alkylic fragment if anchor in frag1: amine = frag1 alkyl = frag2 else: amine = frag2 alkyl = frag1 amine.delete_atom(anchor.bonds[-1].other_end(anchor)) # Change the capping hydrogen into X # X is the same atom type as **anchor** alkyl_cap = alkyl[-1] alkyl_cap.atnum = anchor.atnum cap_bond = alkyl_cap.bonds[0] bond_length = alkyl_cap.radius + cap_bond.other_end(alkyl_cap).radius cap_bond.resize(alkyl_cap, bond_length) # Change X into XH_n alkyl_with_h = add_Hs(alkyl) properties = mol[1].properties for at in alkyl_with_h.atoms[len(alkyl):]: cap_h = Atom(atnum=at.atnum, coords=at.coords, mol=alkyl, settings=properties.copy()) cap_h.properties.pdb_info.IsHeteroAtom = False cap_h.properties.pdb_info.Name = 'Hxx' alkyl.add_atom(cap_h) alkyl.add_bond(Bond(alkyl_cap, cap_h, mol=alkyl)) # Get the match parameters run_match_job(amine, s) run_match_job(alkyl, s) # Set the total charge of the system to 0 anchor.properties.charge_float -= sum(at.properties.charge_float for at in mol) return None
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
def _besides_ring(atom: Atom) -> bool: """Check if any neighboring atoms are part of a ring system.""" return any(plams_mol.in_ring(at) for at in atom.neighbors())
def format_atom(atom: plams.Atom) -> str: symbol, mass, coords = atom.symbol, atom._getmass( ), atom.coords return '{:2s}{:12.4f}{:14.6f}{:14.6f}{:14.6f}\n'.format( symbol, mass, *coords)
def create_molecule(name, r=2.25): mol = Molecule() mol.add_atom(Atom(symbol='Ba', coords=(0, 0, 0))) mol.add_atom(Atom(symbol='F', coords=(0, 0, r))) return mol
AtomNamesList = kf.read('Molecule', 'AtomicNumbers') AtomSymbolList = kf.read('Molecule', 'AtomSymbols') Coords = Units.convert(kf.read('Molecule', 'Coords'), 'Bohr', 'Angstrom') energy = Units.convert(kf.read('AMSResults', 'Energy'), 'au', 'kcal/mol') # 1 Hartree = 1 a.u npCoords = np.array(Coords) npCoords = npCoords.reshape(int(npCoords.size / 3), 3) mol = Molecule() AtomSymbolList = AtomSymbolList.strip() SymbolList = list(AtomSymbolList.split()) SymbolList = [s.strip() for s in SymbolList] for at, coord in zip(SymbolList, npCoords): mol.add_atom(Atom(symbol=at, coords=coord)) # Keep in mind, that coordinate indexing starts from 0; pes_at1 = Atom(symbol=SymbolList[pes_atom1_id - 1], coords=npCoords[pes_atom1_id - 1]) pes_at2 = Atom(symbol=SymbolList[pes_atom2_id - 1], coords=npCoords[pes_atom2_id - 1]) pes_bond = Bond(pes_at1, pes_at2) line = [(pes_atom1_id, pes_atom2_id), direction, PESnum, pes_bond.length(), energy] data.append(line) column_names = [ 'id', 'str/sq', 'PES', 'Bond length [A]', 'Energy [kcal / mol]' ] table_data = pd.DataFrame(data, columns=column_names)
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)