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 split_mol(plams_mol: Molecule, anchor: Atom) -> List[Bond]: """Split a molecule into multiple smaller fragments; returning the bonds that have to be broken. One fragment is created for every branch within **plams_mol**. Parameters ---------- plams_mol : |plams.Molecule| The input molecule. anchor : |plams.Atom| An anchor atom which will be stored in the largest to-be returned fragment. Returns ------- :class:`list` [|plams.Bond|] A list of plams bonds. """ def _in_ring(bond: Bond) -> bool: """Check if **bond** is part of a ring system.""" return plams_mol.in_ring(bond) 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 _is_valid_bond(bond: Bond) -> bool: """Check if one atom in **bond** has at least 3 neighbours and the other at least 2.""" n1, n2 = plams_mol.neighbors(bond.atom1), plams_mol.neighbors( bond.atom2) return (len(n1) >= 3 and len(n2) >= 2) or (len(n1) >= 2 and len(n2) >= 3) def _get_frag_size(bond: Bond) -> int: """Return the size of the largest fragment were **plams_mol** to be split along **bond**.""" if getattr(bond, '_besides_ring', False): del bond._besides_ring return np.inf return plams_mol.get_frag_size(bond, anchor) # Temporary remove hydrogen atoms atom_gen = (at for at in plams_mol if at.atnum == 1) with RemoveAtoms(plams_mol, atom_gen): # Remove undesired bonds bond_list = [bond for bond in plams_mol.bonds if not _in_ring(bond)] # Remove even more undesired bonds for bond in reversed(bond_list): if not _is_valid_bond(bond): bond_list.remove(bond) atom_list = list( itertools.chain.from_iterable( (bond.atom1, bond.atom2) for bond in bond_list)) atom_set = {atom for atom in atom_list if atom_list.count(atom) >= 3} atom_dict = { atom: [bond for bond in atom.bonds if bond in bond_list] for atom in atom_set } for b in bond_list: if plams_mol.in_ring(b.atom1): key = b.atom1 elif plams_mol.in_ring(b.atom2): key = b.atom2 else: continue b._besides_ring = True if key not in atom_dict: atom_dict[key] = 3 * [b] else: atom_dict[key] += (3 - len(atom_dict[key])) * [b] # Fragment the molecule such that the anchor on the largest fragment ret = [] for at, bond_list in atom_dict.items(): # Can't directly iterate over bond_list as its size is modified iterator = range(len(bond_list[2:])) for _ in iterator: frag_size = [_get_frag_size(bond) for bond in bond_list] idx = np.argmax(frag_size) # The index of the largest fragment bond = bond_list.pop(idx) ret.append(bond) return ret