MayaChemTools

    1 #
    2 # File: RDKitUtil.py
    3 # Author: Manish Sud <msud@san.rr.com>
    4 #
    5 # Copyright (C) 2026 Manish Sud. All rights reserved.
    6 #
    7 # The functionality available in this file is implemented using RDKit, an
    8 # open source toolkit for cheminformatics developed by Greg Landrum.
    9 #
   10 # This file is part of MayaChemTools.
   11 #
   12 # MayaChemTools is free software; you can redistribute it and/or modify it under
   13 # the terms of the GNU Lesser General Public License as published by the Free
   14 # Software Foundation; either version 3 of the License, or (at your option) any
   15 # later version.
   16 #
   17 # MayaChemTools is distributed in the hope that it will be useful, but without
   18 # any warranty; without even the implied warranty of merchantability of fitness
   19 # for a particular purpose.  See the GNU Lesser General Public License for more
   20 # details.
   21 #
   22 # You should have received a copy of the GNU Lesser General Public License
   23 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
   24 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
   25 # Boston, MA, 02111-1307, USA.
   26 #
   27 
   28 from __future__ import print_function
   29 
   30 import re
   31 import base64
   32 import json
   33 
   34 from rdkit import Chem
   35 from rdkit.Chem import AllChem
   36 from rdkit.Chem import Draw
   37 
   38 import MiscUtil
   39 
   40 __all__ = [
   41     "AreAtomIndicesSequentiallyConnected",
   42     "AreAtomMapNumbersPresentInMol",
   43     "AreHydrogensMissingInMolecule",
   44     "CalculateFormalCharge",
   45     "CalculateSpinMultiplicity",
   46     "ClearAtomMapNumbers",
   47     "ConstrainAndEmbed",
   48     "FilterSubstructureMatchByAtomMapNumbers",
   49     "FilterSubstructureMatchesByAtomMapNumbers",
   50     "GetAtomIndices",
   51     "GetAtomMapIndices",
   52     "GetAtomMapIndicesAndMapNumbers",
   53     "GetAtomSymbols",
   54     "GetAtomPositions",
   55     "GetFormalCharge",
   56     "GetHeavyAtomNeighbors",
   57     "GetInlineSVGForMolecule",
   58     "GetInlineSVGForMolecules",
   59     "GetMolName",
   60     "GetNumFragments",
   61     "GetNumHeavyAtomNeighbors",
   62     "GetSpinMultiplicity",
   63     "GetSVGForMolecule",
   64     "GetSVGForMolecules",
   65     "GetPsi4XYZFormatString",
   66     "GetTorsionsAroundRotatableBonds",
   67     "GenerateBase64EncodedMolStrings",
   68     "GenerateBase64EncodedMolStringWithConfIDs",
   69     "IsAtomSymbolPresentInMol",
   70     "IsMolEmpty",
   71     "IsValidElementSymbol",
   72     "IsValidAtomIndex",
   73     "MolFromBase64EncodedMolString",
   74     "GenerateBase64EncodedMolStringsWithIDs",
   75     "MolToBase64EncodedMolString",
   76     "MolFromSubstructureMatch",
   77     "MolsFromSubstructureMatches",
   78     "ReadMolecules",
   79     "ReadAndValidateMolecules",
   80     "ReadMoleculesFromSDFile",
   81     "ReadMoleculesFromMolFile",
   82     "ReadMoleculesFromMol2File",
   83     "ReadMoleculesFromPDBFile",
   84     "ReadMoleculesFromSMILESFile",
   85     "ReorderAtomIndicesInSequentiallyConnectedManner",
   86     "SetAtomPositions",
   87     "SetupHTMLForTorsionScanViewer",
   88     "SetWriterMolProps",
   89     "ValidateElementSymbols",
   90     "WriteMolecules",
   91 ]
   92 
   93 
   94 def GetMolName(Mol, MolNum=None):
   95     """Get molecule name.
   96 
   97     Arguments:
   98         Mol (object): RDKit molecule object.
   99         MolNum (int or None): Molecule number in input file.
  100 
  101     Returns:
  102         str : Molname corresponding to _Name property of a molecule, generated
  103             from specieid MolNum using the format "Mol%d" % MolNum, or an
  104             empty string.
  105 
  106     """
  107 
  108     MolName = ""
  109     if Mol.HasProp("_Name"):
  110         MolName = Mol.GetProp("_Name")
  111 
  112     if not len(MolName):
  113         if MolNum is not None:
  114             MolName = "Mol%d" % MolNum
  115 
  116     return MolName
  117 
  118 
  119 def GetInlineSVGForMolecule(
  120     Mol,
  121     Width,
  122     Height,
  123     Legend=None,
  124     AtomListToHighlight=None,
  125     BondListToHighlight=None,
  126     BoldText=True,
  127     Base64Encoded=True,
  128 ):
  129     """Get SVG image text for a molecule suitable for inline embedding into a HTML page.
  130 
  131     Arguments:
  132         Mol (object): RDKit molecule object.
  133         Width (int): Width of a molecule image in pixels.
  134         Height (int): Height of a molecule image in pixels.
  135         Legend (str): Text to display under the image.
  136         AtomListToHighlight (list): List of atoms to highlight.
  137         BondListToHighlight (list): List of bonds to highlight.
  138         BoldText (bool): Flag to make text bold in the image of molecule.
  139         Base64Encoded (bool): Flag to return base64 encoded string.
  140 
  141     Returns:
  142         str : SVG image text for inline embedding into a HTML page using "img"
  143             tag: <img src="data:image/svg+xml;charset=UTF-8,SVGImageText> or
  144             tag: <img src="data:image/svg+xml;base64,SVGImageText>
  145 
  146     """
  147 
  148     SVGText = GetSVGForMolecule(Mol, Width, Height, Legend, AtomListToHighlight, BondListToHighlight, BoldText)
  149     return _ModifySVGForInlineEmbedding(SVGText, Base64Encoded)
  150 
  151 
  152 def GetInlineSVGForMolecules(
  153     Mols,
  154     MolsPerRow,
  155     MolWidth,
  156     MolHeight,
  157     Legends=None,
  158     AtomListsToHighlight=None,
  159     BondListsToHighLight=None,
  160     BoldText=True,
  161     Base64Encoded=True,
  162 ):
  163     """Get SVG image text for  molecules suitable for inline embedding into a HTML page.
  164 
  165     Arguments:
  166         Mols (list): List of RDKit molecule objects.
  167         MolsPerRow (int): Number of molecules per row.
  168         Width (int): Width of a molecule image in pixels.
  169         Height (int): Height of a molecule image in pixels.
  170         Legends (list): List containing strings to display under images.
  171         AtomListsToHighlight (list): List of lists containing atoms to highlight
  172             for molecules.
  173         BondListsToHighlight (list): List of lists containing bonds to highlight
  174             for molecules
  175         BoldText (bool): Flag to make text bold in the image of molecules.
  176         Base64Encoded (bool): Flag to return base64 encoded string.
  177 
  178     Returns:
  179         str : SVG image text for inline embedding into a HTML page using "img"
  180             tag: <img src="data:image/svg+xml;charset=UTF-8,SVGImageText> or
  181             tag: <img src="data:image/svg+xml;base64,SVGImageText>
  182 
  183     """
  184 
  185     SVGText = GetSVGForMolecules(
  186         Mols, MolsPerRow, MolWidth, MolHeight, Legends, AtomListsToHighlight, BondListsToHighLight, BoldText
  187     )
  188     return _ModifySVGForInlineEmbedding(SVGText, Base64Encoded)
  189 
  190 
  191 def _ModifySVGForInlineEmbedding(SVGText, Base64Encoded):
  192     """Modify SVG for inline embedding into a HTML page using "img" tag
  193     along with performing base64 encoding.
  194     """
  195 
  196     # Take out all tags till the start of '<svg' tag...
  197     Pattern = re.compile("^.*<svg", re.I | re.S)
  198     SVGText = Pattern.sub("<svg", SVGText)
  199 
  200     # Add an extra space before the "width=..." tag. Otherwise, inline embedding may
  201     # cause the following XML error on some browsers due to start of the "width=..."
  202     # at the begining of the line in <svg ...> tag:
  203     #
  204     #  XML5607: Whitespace expected.
  205     #
  206     SVGText = re.sub("width='", " width='", SVGText, flags=re.I)
  207 
  208     # Take out trailing new line...
  209     SVGText = SVGText.strip()
  210 
  211     # Perform base64 encoding by turning text into byte stream using string
  212     # encode and transform byte stream returned by b64encode into a string
  213     # by string decode...
  214     #
  215     if Base64Encoded:
  216         SVGText = base64.b64encode(SVGText.encode()).decode()
  217 
  218     return SVGText
  219 
  220 
  221 def GetSVGForMolecule(
  222     Mol, Width, Height, Legend=None, AtomListToHighlight=None, BondListToHighlight=None, BoldText=True
  223 ):
  224     """Get SVG image text for a molecule suitable for viewing in a browser.
  225 
  226     Arguments:
  227         Mol (object): RDKit molecule object.
  228         Width (int): Width of a molecule image in pixels.
  229         Height (int): Height of a molecule image in pixels.
  230         Legend (str): Text to display under the image.
  231         AtomListToHighlight (list): List of atoms to highlight.
  232         BondListToHighlight (list): List of bonds to highlight.
  233         BoldText (bool): Flag to make text bold in the image of molecule.
  234 
  235     Returns:
  236         str : SVG image text for writing to a SVG file for viewing in a browser.
  237 
  238     """
  239 
  240     Mols = [Mol]
  241 
  242     MolsPerRow = 1
  243     MolWidth = Width
  244     MolHeight = Height
  245 
  246     Legends = [Legend] if Legend is not None else None
  247     AtomListsToHighlight = [AtomListToHighlight] if AtomListToHighlight is not None else None
  248     BondListsToHighLight = [BondListToHighlight] if BondListToHighlight is not None else None
  249 
  250     return GetSVGForMolecules(
  251         Mols, MolsPerRow, MolWidth, MolHeight, Legends, AtomListsToHighlight, BondListsToHighLight, BoldText
  252     )
  253 
  254 
  255 def GetSVGForMolecules(
  256     Mols,
  257     MolsPerRow,
  258     MolWidth,
  259     MolHeight,
  260     Legends=None,
  261     AtomListsToHighlight=None,
  262     BondListsToHighlight=None,
  263     BoldText=True,
  264 ):
  265     """Get SVG image text for molecules suitable for viewing in a browser.
  266 
  267     Arguments:
  268         Mols (list): List of RDKit molecule objects.
  269         MolsPerRow (int): Number of molecules per row.
  270         Width (int): Width of a molecule image in pixels.
  271         Height (int): Height of a molecule image in pixels.
  272         Legends (list): List containing strings to display under images.
  273         AtomListsToHighlight (list): List of lists containing atoms to highlight
  274             for molecules.
  275         BondListsToHighlight (list): List of lists containing bonds to highlight
  276             for molecules
  277         BoldText (bool): Flag to make text bold in the image of molecules.
  278 
  279     Returns:
  280         str : SVG image text for writing to a SVG file for viewing in a browser.
  281 
  282     """
  283 
  284     SVGText = Draw.MolsToGridImage(
  285         Mols,
  286         molsPerRow=MolsPerRow,
  287         subImgSize=(MolWidth, MolHeight),
  288         legends=Legends,
  289         highlightAtomLists=AtomListsToHighlight,
  290         highlightBondLists=BondListsToHighlight,
  291         useSVG=True,
  292     )
  293 
  294     return _ModifySVGForBrowserViewing(SVGText, BoldText)
  295 
  296 
  297 def _ModifySVGForBrowserViewing(SVGText, BoldText=True):
  298     """Modify SVG for loading into a browser."""
  299 
  300     # It appears that the string 'xmlns:svg' needs to be replaced with 'xmlns' in the
  301     # SVG image string generated by older versions of RDKit. Otherwise, the image
  302     # doesn't load in web browsers.
  303     #
  304     if re.search("xmlns:svg", SVGText, re.I):
  305         SVGText = re.sub("xmlns:svg", "xmlns", SVGText, flags=re.I)
  306 
  307     # Make text bold...
  308     if BoldText:
  309         SVGText = re.sub("font-weight:normal;", "font-weight:bold;", SVGText, flags=re.I)
  310 
  311     return SVGText
  312 
  313 
  314 def IsMolEmpty(Mol):
  315     """Check for the presence of atoms in a molecule.
  316 
  317     Arguments:
  318         Mol (object): RDKit molecule object.
  319 
  320     Returns:
  321         bool : True - No atoms in molecule; Otherwise, false.
  322 
  323     """
  324 
  325     Status = False if Mol.GetNumAtoms() else True
  326 
  327     return Status
  328 
  329 
  330 def IsAtomSymbolPresentInMol(Mol, AtomSymbol, IgnoreCase=True):
  331     """Check for the presence of an atom symbol in a molecule.
  332 
  333     Arguments:
  334         Mol (object): RDKit molecule object.
  335         AtomSymbol (str): Atom symbol.
  336 
  337     Returns:
  338         bool : True - Atom symbol in molecule; Otherwise, false.
  339 
  340     """
  341 
  342     for Atom in Mol.GetAtoms():
  343         Symbol = Atom.GetSymbol()
  344         if IgnoreCase:
  345             if re.match("^%s$" % AtomSymbol, Symbol, re.I):
  346                 return True
  347         else:
  348             if re.match("^%s$" % AtomSymbol, Symbol):
  349                 return True
  350 
  351     return False
  352 
  353 
  354 def ValidateElementSymbols(ElementSymbols):
  355     """Validate element symbols.
  356 
  357     Arguments:
  358         ElementSymbols (list): List of element symbols to validate.
  359 
  360     Returns:
  361         bool : True - All element symbols are valid; Otherwise, false.
  362 
  363     """
  364     for ElementSymbol in ElementSymbols:
  365         if not IsValidElementSymbol(ElementSymbol):
  366             return False
  367 
  368     return True
  369 
  370 
  371 def GetAtomPositions(Mol, ConfID=-1):
  372     """Retrieve a list of lists containing coordinates of all atoms in a
  373     molecule.
  374 
  375     Arguments:
  376         Mol (object): RDKit molecule object.
  377         ConfID (int): Conformer number.
  378 
  379     Returns:
  380         list : List of lists containing atom positions.
  381 
  382     Examples:
  383 
  384         for AtomPosition in RDKitUtil.GetAtomPositions(Mol):
  385             print("X: %s; Y: %s; Z: %s" % (AtomPosition[0], AtomPosition[1], AtomPosition[2]))
  386 
  387     """
  388 
  389     return Mol.GetConformer(id=ConfID).GetPositions().tolist()
  390 
  391 
  392 def SetAtomPositions(Mol, AtomPositions, ConfID=-1):
  393     """Set atom positions of all atoms in a molecule.
  394 
  395     Arguments:
  396         Mol (object): RDKit molecule object.
  397         AtomPositions (object): List of lists containing atom positions.
  398         ConfID (int): Conformer number.
  399 
  400     Returns:
  401         object : RDKit molecule object.
  402 
  403     """
  404 
  405     MolConf = Mol.GetConformer(ConfID)
  406 
  407     for Index in range(len(AtomPositions)):
  408         MolConf.SetAtomPosition(Index, tuple(AtomPositions[Index]))
  409 
  410     return Mol
  411 
  412 
  413 def GetAtomSymbols(Mol):
  414     """Retrieve a list containing atom symbols of all atoms a molecule.
  415 
  416     Arguments:
  417         Mol (object): RDKit molecule object.
  418 
  419     Returns:
  420         list : List of atom symbols.
  421 
  422     """
  423 
  424     return [Atom.GetSymbol() for Atom in Mol.GetAtoms()]
  425 
  426 
  427 def GetAtomIndices(Mol):
  428     """Retrieve a list containing atom indices of all atoms a molecule.
  429 
  430     Arguments:
  431         Mol (object): RDKit molecule object.
  432 
  433     Returns:
  434         list : List of atom indices.
  435 
  436     """
  437 
  438     return [Atom.GetIdx() for Atom in Mol.GetAtoms()]
  439 
  440 
  441 def GetFormalCharge(Mol, CheckMolProp=True):
  442     """Get formal charge of a molecule. The formal charge is either retrieved
  443     from 'FormalCharge' molecule property or calculated using RDKit function
  444     Chem.GetFormalCharge(Mol).
  445 
  446     The 'FormalCharge' molecule property may contain multiple space delimited
  447     values. The total formal charge corresponds to the sum of the specified formal
  448     charge values.
  449 
  450     Arguments:
  451         Mol (object): RDKit molecule object.
  452         CheckMolProp (bool): Check 'FormalCharge' molecule property to
  453             retrieve formal charge.
  454 
  455     Returns:
  456         int : Formal charge.
  457 
  458     """
  459 
  460     Name = "FormalCharge"
  461     if CheckMolProp and Mol.HasProp(Name):
  462         FormalCharge = Mol.GetProp(Name)
  463         Values = FormalCharge.split()
  464         if len(Values) > 1:
  465             MiscUtil.PrintWarning(
  466                 "RDKitUtil.GetFormalCharge: Molecule property, %s, contains multiple values, %s. Formal charge corresponds to sum of the specified values..."
  467                 % (Name, FormalCharge)
  468             )
  469             FormalCharge = 0.0
  470             for Value in Values:
  471                 FormalCharge += float(Value)
  472             FormalCharge = int(FormalCharge)
  473         else:
  474             FormalCharge = int(float(FormalCharge))
  475     else:
  476         FormalCharge = CalculateFormalCharge(Mol)
  477 
  478     return int(FormalCharge)
  479 
  480 
  481 def CalculateFormalCharge(Mol):
  482     """Calculate formal charge of a molecule. The formal charge is calculated
  483     using RDKit function Chem.GetFormalCharge(Mol).
  484 
  485     Arguments:
  486         Mol (object): RDKit molecule object.
  487             retrieve formal charge.
  488 
  489     Returns:
  490         int : Formal charge.
  491 
  492     """
  493 
  494     return int(Chem.GetFormalCharge(Mol))
  495 
  496 
  497 def GetSpinMultiplicity(Mol, CheckMolProp=True):
  498     """Get spin multiplicity of a molecule. The spin multiplicity is either
  499     retrieved from 'SpinMultiplicity' molecule property or calculated
  500     from the number of free radical electrons using Hund's rule of maximum
  501     multiplicity defined as 2S + 1 where S is the total electron spin. The
  502     total spin is 1/2 the number of free radical electrons in a molecule.
  503 
  504     The 'SpinMultiplicity' molecule property may contain multiple space delimited
  505     values. The total spin multiplicity corresponds to the total number of free radical
  506     electrons which are calculated for each specified value.
  507 
  508     Arguments:
  509         Mol (object): RDKit molecule object.
  510         CheckMolProp (bool): Check 'SpinMultiplicity' molecule property to
  511             retrieve spin multiplicity.
  512 
  513     Returns:
  514         int : Spin multiplicity.
  515 
  516     """
  517 
  518     Name = "SpinMultiplicity"
  519     if CheckMolProp and Mol.HasProp(Name):
  520         SpinMultiplicity = Mol.GetProp(Name)
  521         Values = SpinMultiplicity.split()
  522         if len(Values) > 1:
  523             MiscUtil.PrintWarning(
  524                 "RDKitUtil.GetSpinMultiplicity: Molecule property, %s, contains multiple values, %s. Calculating spin multiplicity corresponding to total number of free radical electrons for each specified value..."
  525                 % (Name, SpinMultiplicity)
  526             )
  527             NumRadicalElectrons = 0
  528             for Value in Values:
  529                 NumRadicalElectrons += int(float(Value)) - 1
  530 
  531             TotalElectronicSpin = NumRadicalElectrons / 2
  532             SpinMultiplicity = 2 * TotalElectronicSpin + 1
  533         else:
  534             SpinMultiplicity = int(float(SpinMultiplicity))
  535     else:
  536         SpinMultiplicity = CalculateSpinMultiplicity(Mol)
  537 
  538     return int(SpinMultiplicity)
  539 
  540 
  541 def CalculateSpinMultiplicity(Mol):
  542     """Calculate spin multiplicity of a molecule. The spin multiplicity is calculated
  543     from the number of free radical electrons using Hund's rule of maximum
  544     multiplicity defined as 2S + 1 where S is the total electron spin. The
  545     total spin is 1/2 the number of free radical electrons in a molecule.
  546 
  547     Arguments:
  548         Mol (object): RDKit molecule object.
  549 
  550     Returns:
  551         int : Spin multiplicity.
  552 
  553     """
  554 
  555     # Calculate spin multiplicity using Hund's rule of maximum multiplicity...
  556     NumRadicalElectrons = 0
  557     for Atom in Mol.GetAtoms():
  558         NumRadicalElectrons += Atom.GetNumRadicalElectrons()
  559 
  560     TotalElectronicSpin = NumRadicalElectrons / 2
  561     SpinMultiplicity = 2 * TotalElectronicSpin + 1
  562 
  563     return int(SpinMultiplicity)
  564 
  565 
  566 def GetPsi4XYZFormatString(
  567     Mol,
  568     ConfID=-1,
  569     FormalCharge="auto",
  570     SpinMultiplicity="auto",
  571     Symmetry="auto",
  572     NoCom=False,
  573     NoReorient=False,
  574     CheckFragments=False,
  575 ):
  576     """Retrieve geometry string of a molecule in Psi4ish XYZ format to perform
  577     Psi4 quantum chemistry calculations.
  578 
  579     You may explicit specify multiple space delimited values for formal charge
  580     and spin multiplicity. Otherwise, these values are either automatically
  581     retrieved from 'FormalCharge' and 'SpinMultiplicity' molecule properties or
  582     calculated using RDKit. The number of specified values for these properties
  583     must match the number of fragments in the molecule during the processing
  584     of the fragments.
  585 
  586     Arguments:
  587         Mol (object): RDKit molecule object.
  588         ConfID (int): Conformer number.
  589         FormalCharge (str): Specified formal charge or 'auto' to calculate
  590            its value by RDKit.
  591         SpinMultiplicity (str): Specified spin multiplicity or 'auto' to calculate
  592            its value by RDKit.
  593         Symmetry (str): Specified symmetry or 'auto' to calculate its value by
  594            Psi4.
  595         NoCom (bool): Flag to disable recentering of a molecule by Psi4.
  596         NoReorient (bool): Flag to disable reorientation of a molecule by Psi4.
  597         CheckFragments (bool): Check for fragments and setup geometry string
  598            using  -- separator between fragments.
  599 
  600     Returns:
  601         str : Geometry string of a molecule in Psi4ish XYZ format.
  602 
  603     """
  604 
  605     # Check for fragments...
  606     Mols = [Mol]
  607     if CheckFragments:
  608         Fragments = list(Chem.rdmolops.GetMolFrags(Mol, asMols=True))
  609         if len(Fragments) > 1:
  610             Mols = Fragments
  611 
  612     FragMolFormalCharges = _SetFormalChargesForPsi4XYZFormatString(Mol, Mols, FormalCharge, CheckFragments)
  613     FragMolSpinMultiplicities = _SetSpinMultiplicitiesForPsi4XYZFormatString(
  614         Mol, Mols, SpinMultiplicity, CheckFragments
  615     )
  616 
  617     # Setup geometry string for Ps4...
  618     GeometryList = []
  619     FragMolCount = 0
  620 
  621     for FragMolIndex, FragMol in enumerate(Mols):
  622         FragMolCount += 1
  623         if FragMolCount > 1:
  624             GeometryList.append("--")
  625 
  626         FragMolFormalCharge = FragMolFormalCharges[FragMolIndex]
  627         FragMolSpinMultiplicity = FragMolSpinMultiplicities[FragMolIndex]
  628         if FragMolFormalCharge is None or FragMolSpinMultiplicity is None:
  629             MiscUtil.PrintInfo("")
  630             MiscUtil.PrintWarning(
  631                 "RDKitUtil.GetPsi4XYZFormatString: Failed to set formal charge and spin multiplicity values. Both formal charge, %s, and spin multiplicity, %s, must be valid values. These values are either specified explicitly or automatically calculated..."
  632                 % (FragMolFormalCharge, FragMolSpinMultiplicity)
  633             )
  634         else:
  635             GeometryList.append("%s %s" % (FragMolFormalCharge, FragMolSpinMultiplicity))
  636 
  637         AtomSymbols = GetAtomSymbols(FragMol)
  638         AtomPositions = GetAtomPositions(FragMol, ConfID)
  639 
  640         for AtomSymbol, AtomPosition in zip(AtomSymbols, AtomPositions):
  641             GeometryList.append("%s %s %s %s" % (AtomSymbol, AtomPosition[0], AtomPosition[1], AtomPosition[2]))
  642 
  643     GeometryList.append("units angstrom")
  644 
  645     if not re.match("^auto$", Symmetry, re.I):
  646         Name = "Symmetry"
  647         if Mol.HasProp(Name):
  648             Symmetry = Mol.GetProp(Name)
  649         GeometryList.append("symmetry %s" % Symmetry)
  650 
  651     if NoCom:
  652         GeometryList.append("no_com")
  653 
  654     if NoReorient:
  655         GeometryList.append("no_reorient")
  656 
  657     Geometry = "\n".join(GeometryList)
  658 
  659     return Geometry
  660 
  661 
  662 def _SetFormalChargesForPsi4XYZFormatString(Mol, FragMols, FormalCharge, CheckFragments):
  663     """Setup formal charges for Psi4 XYZ format string."""
  664 
  665     if not CheckFragments:
  666         if re.match("^auto$", FormalCharge, re.I):
  667             MolFormalCharge = GetFormalCharge(Mol)
  668         else:
  669             MolFormalCharge = int(FormalCharge)
  670         return [MolFormalCharge]
  671 
  672     FragMolsCount = len(FragMols)
  673     FormalCharges = [None] * FragMolsCount
  674 
  675     if re.match("^auto$", FormalCharge, re.I):
  676         PropName = "FormalCharge"
  677         if Mol.HasProp(PropName):
  678             FormalCharge = Mol.GetProp(PropName)
  679             FormalChargeWords = FormalCharge.split()
  680             if len(FormalChargeWords) == FragMolsCount:
  681                 FormalCharges = [int(float(FormalCharge)) for FormalCharge in FormalChargeWords]
  682             else:
  683                 MiscUtil.PrintWarning(
  684                     "RDKitUtil.GetPsi4XYZFormatString: Ignoring specified value, %s, for FormalCharge molecule property. The number of space delimted specified values, %s,  must match number of fragments, %s, in the molecule..."
  685                     % (FormalCharge, len(FormalChargeWords), FragMolsCount)
  686                 )
  687         else:
  688             FormalCharges = [CalculateFormalCharge(FragMol) for FragMol in FragMols]
  689     else:
  690         FormalChargeWords = FormalCharge.split()
  691         if len(FormalChargeWords) != FragMolsCount:
  692             MiscUtil.PrintWarning(
  693                 "RDKitUtil.GetPsi4XYZFormatString: Ignoring specified value, %s, for FormalCharge paramater. The number of space delimted specified values, %s,  must match number of fragments, %s, in the molecule..."
  694                 % (FormalCharge, len(FormalChargeWords), FragMolsCount)
  695             )
  696         else:
  697             FormalCharges = [int(FormalCharge) for FormalCharge in FormalChargeWords]
  698 
  699     return FormalCharges
  700 
  701 
  702 def _SetSpinMultiplicitiesForPsi4XYZFormatString(Mol, FragMols, SpinMultiplicity, CheckFragments):
  703     """Setup spin multiplicites for Psi4 XYZ format string."""
  704 
  705     if not CheckFragments:
  706         if re.match("^auto$", SpinMultiplicity, re.I):
  707             MolSpinMultiplicity = GetSpinMultiplicity(Mol)
  708         else:
  709             MolSpinMultiplicity = int(SpinMultiplicity)
  710         return [MolSpinMultiplicity]
  711 
  712     FragMolsCount = len(FragMols)
  713     SpinMultiplicities = [None] * FragMolsCount
  714 
  715     if re.match("^auto$", SpinMultiplicity, re.I):
  716         PropName = "SpinMultiplicity"
  717         if Mol.HasProp(PropName):
  718             SpinMultiplicity = Mol.GetProp(PropName)
  719             SpinMultiplicityWords = SpinMultiplicity.split()
  720             if len(SpinMultiplicityWords) == FragMolsCount:
  721                 SpinMultiplicities = [int(float(SpinMultiplicity)) for SpinMultiplicity in SpinMultiplicityWords]
  722             else:
  723                 MiscUtil.PrintWarning(
  724                     "RDKitUtil.GetPsi4XYZFormatString: Ignoring specified value, %s, for SpinMultiplicity molecule property. The number of space delimted specified values, %s,  must match number of fragments, %s, in the molecule..."
  725                     % (SpinMultiplicity, len(SpinMultiplicityWords), FragMolsCount)
  726                 )
  727         else:
  728             SpinMultiplicities = [CalculateSpinMultiplicity(FragMol) for FragMol in FragMols]
  729     else:
  730         SpinMultiplicityWords = SpinMultiplicity.split()
  731         if len(SpinMultiplicityWords) != FragMolsCount:
  732             MiscUtil.PrintWarning(
  733                 "RDKitUtil.GetPsi4XYZFormatString: Ignoring specified value, %s, for SpinMultiplicity paramater. The number of space delimted specified values, %s,  must match number of fragments, %s, in the molecule..."
  734                 % (SpinMultiplicity, len(SpinMultiplicityWords), FragMolsCount)
  735             )
  736         else:
  737             SpinMultiplicities = [int(SpinMultiplicity) for SpinMultiplicity in SpinMultiplicityWords]
  738 
  739     return SpinMultiplicities
  740 
  741 
  742 def GetTorsionsAroundRotatableBonds(Mol, RotBondsPatternMol, IgnoreHydrogens=True):
  743     """Identify torsions around rotatable bonds and return a list of lists
  744     containing atom indices of torsions.
  745 
  746     Arguments:
  747         Mol (object): RDKit molecule object.
  748         PatternMol (object): RDKit molecule object corresponding to SMARTS
  749             pattern to identify rotatable bonds.
  750         IgnoreHydrogens (bool): Flag to include torsions around rotatable bonds
  751             containing hydrogens.
  752 
  753     Returns:
  754         list : List of lists containing atom indices of torsions around
  755             rotatable bonds.
  756 
  757     """
  758 
  759     # Match rotatable bonds...
  760     RotBondsMatches = FilterSubstructureMatchesByAtomMapNumbers(
  761         Mol, RotBondsPatternMol, Mol.GetSubstructMatches(RotBondsPatternMol, useChirality=False)
  762     )
  763     if not len(RotBondsMatches):
  764         return None
  765 
  766     # Identify torsions...
  767     TorsioAtomIndicesList = []
  768     for RotBondMatch in RotBondsMatches:
  769         if len(RotBondMatch) != 2:
  770             continue
  771 
  772         Atom1Index, Atom2Index = RotBondMatch
  773         Atom1NbrIndices = _GetRotBondAtomNeighbors(Mol, Atom1Index, Atom2Index, IgnoreHydrogens)
  774         Atom2NbrIndices = _GetRotBondAtomNeighbors(Mol, Atom2Index, Atom1Index, IgnoreHydrogens)
  775 
  776         TorsionAtomIndices = []
  777         for Atom1NbrIndex in Atom1NbrIndices:
  778             if Atom1NbrIndex in Atom2NbrIndices:
  779                 continue
  780             for Atom2NbrIndex in Atom2NbrIndices:
  781                 if Atom2NbrIndex in Atom1NbrIndices:
  782                     continue
  783                 TorsionAtomIndices.append([Atom1NbrIndex, Atom1Index, Atom2Index, Atom2NbrIndex])
  784 
  785         TorsioAtomIndicesList.extend(TorsionAtomIndices)
  786 
  787     return TorsioAtomIndicesList
  788 
  789 
  790 def _GetRotBondAtomNeighbors(Mol, AtomIndex, BondedAtomIndex, IgnoreHydrogens):
  791     """Get atom neigbors around a rotatable bond."""
  792 
  793     Atom = Mol.GetAtomWithIdx(AtomIndex)
  794     AtomNeighbors = []
  795 
  796     for AtomNbr in Atom.GetNeighbors():
  797         AtomNbrIndex = AtomNbr.GetIdx()
  798         if AtomNbrIndex == BondedAtomIndex:
  799             continue
  800         if IgnoreHydrogens:
  801             if AtomNbr.GetAtomicNum() == 1:
  802                 continue
  803 
  804         AtomNeighbors.append(AtomNbrIndex)
  805 
  806     return AtomNeighbors
  807 
  808 
  809 def GetNumFragments(Mol):
  810     """Get number of fragment in a molecule.
  811 
  812     Arguments:
  813         Atom (object): RDKit molecule object.
  814 
  815     Returns:
  816         int : Number of fragments.
  817 
  818     """
  819 
  820     Fragments = Chem.rdmolops.GetMolFrags(Mol, asMols=False)
  821 
  822     return len(Fragments) if Fragments is not None else 0
  823 
  824 
  825 def GetNumHeavyAtomNeighbors(Atom):
  826     """Get number of heavy atom neighbors.
  827 
  828     Arguments:
  829         Atom (object): RDKit atom object.
  830 
  831     Returns:
  832         int : Number of neighbors.
  833 
  834     """
  835 
  836     NbrCount = 0
  837     for AtomNbr in Atom.GetNeighbors():
  838         if AtomNbr.GetAtomicNum() > 1:
  839             NbrCount += 1
  840 
  841     return NbrCount
  842 
  843 
  844 def GetHeavyAtomNeighbors(Atom):
  845     """Get a list of heavy atom neighbors.
  846 
  847     Arguments:
  848         Atom (object): RDKit atom object.
  849 
  850     Returns:
  851         list : List of heavy atom neighbors.
  852 
  853     """
  854 
  855     AtomNeighbors = []
  856     for AtomNbr in Atom.GetNeighbors():
  857         if AtomNbr.GetAtomicNum() > 1:
  858             AtomNeighbors.append(AtomNbr)
  859 
  860     return AtomNeighbors
  861 
  862 
  863 def IsValidElementSymbol(ElementSymbol):
  864     """Validate element symbol.
  865 
  866     Arguments:
  867         ElementSymbol (str): Element symbol
  868 
  869     Returns:
  870         bool : True - Valid element symbol; Otherwise, false.
  871 
  872     """
  873 
  874     try:
  875         AtomicNumber = Chem.GetPeriodicTable().GetAtomicNumber(ElementSymbol)
  876         Status = True if AtomicNumber > 0 else False
  877     except Exception:
  878         Status = False
  879 
  880     return Status
  881 
  882 
  883 def IsValidAtomIndex(Mol, AtomIndex):
  884     """Validate presence  atom index in a molecule.
  885 
  886     Arguments:
  887         Mol (object): RDKit molecule object.
  888         AtomIndex (int): Atom index.
  889 
  890     Returns:
  891         bool : True - Valid atom index; Otherwise, false.
  892 
  893     """
  894     for Atom in Mol.GetAtoms():
  895         if AtomIndex == Atom.GetIdx():
  896             return True
  897 
  898     return False
  899 
  900 
  901 def AreHydrogensMissingInMolecule(Mol):
  902     """Check for any missing hydrogens in  in a molecue.
  903 
  904     Arguments:
  905         Mol (object): RDKit molecule object.
  906 
  907     Returns:
  908         bool : True - Missing hydrogens; Otherwise, false.
  909 
  910     """
  911 
  912     for Atom in Mol.GetAtoms():
  913         NumExplicitAndImplicitHs = Atom.GetNumExplicitHs() + Atom.GetNumImplicitHs()
  914         if NumExplicitAndImplicitHs > 0:
  915             return True
  916 
  917     return False
  918 
  919 
  920 def AreAtomIndicesSequentiallyConnected(Mol, AtomIndices):
  921     """Check for the presence bonds between sequential pairs of atoms in a
  922     molecule.
  923 
  924     Arguments:
  925         Mol (object): RDKit molecule object.
  926         AtomIndices (list): List of atom indices.
  927 
  928     Returns:
  929         bool : True - Sequentially connected; Otherwise, false.
  930 
  931     """
  932 
  933     for Index in range(0, (len(AtomIndices) - 1)):
  934         Bond = Mol.GetBondBetweenAtoms(AtomIndices[Index], AtomIndices[Index + 1])
  935         if Bond is None:
  936             return False
  937 
  938         if Bond.GetIdx() is None:
  939             return False
  940 
  941     return True
  942 
  943 
  944 def ReorderAtomIndicesInSequentiallyConnectedManner(Mol, AtomIndices):
  945     """Check for the presence of sequentially connected list of atoms in an
  946     arbitray list of atoms in molecule.
  947 
  948     Arguments:
  949         Mol (object): RDKit molecule object.
  950         AtomIndices (list): List of atom indices.
  951 
  952     Returns:
  953         bool : True - Sequentially connected list found; Otherwise, false.
  954         list : List of seqeuntially connected atoms or None.
  955 
  956     """
  957 
  958     # Count the number of neighbors for specified atom indices ensuring
  959     # that the neighbors are also part of atom indices...
  960     AtomNbrsCount = {}
  961     for AtomIndex in AtomIndices:
  962         Atom = Mol.GetAtomWithIdx(AtomIndex)
  963 
  964         AtomNbrsCount[AtomIndex] = 0
  965         for AtomNbr in Atom.GetNeighbors():
  966             AtomNbrIndex = AtomNbr.GetIdx()
  967             if AtomNbrIndex not in AtomIndices:
  968                 continue
  969             AtomNbrsCount[AtomIndex] += 1
  970 
  971     # Number of neighbors for each specified atom indices must be 1 or 2
  972     # for sequentially connected list of atom indices...
  973     AtomsWithOneNbr = []
  974     for AtomIndex, NbrsCount in AtomNbrsCount.items():
  975         if not (NbrsCount == 1 or NbrsCount == 2):
  976             return (False, None)
  977 
  978         if NbrsCount == 1:
  979             AtomsWithOneNbr.append(AtomIndex)
  980 
  981     # A sequentially connected list of indices must have two atom indices with
  982     # exactly # one neighbor...
  983     if len(AtomsWithOneNbr) != 2:
  984         return (False, None)
  985 
  986     # Setup a reordered list of sequentially connected atoms...
  987     ReorderedAtomIndices = []
  988 
  989     AtomIndex1, AtomIndex2 = AtomsWithOneNbr
  990     AtomIndex = AtomIndex1 if AtomIndex1 < AtomIndex2 else AtomIndex2
  991     ReorderedAtomIndices.append(AtomIndex)
  992 
  993     while len(ReorderedAtomIndices) < len(AtomIndices):
  994         Atom = Mol.GetAtomWithIdx(AtomIndex)
  995 
  996         for AtomNbr in Atom.GetNeighbors():
  997             AtomNbrIndex = AtomNbr.GetIdx()
  998             if AtomNbrIndex not in AtomIndices:
  999                 continue
 1000 
 1001             if AtomNbrIndex in ReorderedAtomIndices:
 1002                 continue
 1003 
 1004             # Treat neighbor as next connected atom...
 1005             AtomIndex = AtomNbrIndex
 1006             ReorderedAtomIndices.append(AtomIndex)
 1007             break
 1008 
 1009     # Check reorderd list size...
 1010     if len(ReorderedAtomIndices) != len(AtomIndices):
 1011         return (False, None)
 1012 
 1013     # A final check to validate reorderd list...
 1014     if not AreAtomIndicesSequentiallyConnected(Mol, ReorderedAtomIndices):
 1015         return (False, None)
 1016 
 1017     return (True, ReorderedAtomIndices)
 1018 
 1019 
 1020 def MolToBase64EncodedMolString(Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.AllProps):
 1021     """Encode RDkit molecule object into a base64 encoded string. The properties
 1022     can be optionally excluded.
 1023 
 1024     The molecule is pickled using RDKit Mol.ToBinary() function before
 1025     their encoding.
 1026 
 1027     Arguments:
 1028         Mol (object): RDKit molecule object.
 1029         PropertyPickleFlags: RDKit property pickle options.
 1030 
 1031     Returns:
 1032         str : Base64 encode molecule string or None.
 1033 
 1034     Notes:
 1035         The following property pickle flags are currently available in RDKit:
 1036 
 1037             Chem.PropertyPickleOptions.NoProps
 1038             Chem.PropertyPickleOptions.MolProps
 1039             Chem.PropertyPickleOptions.AtomProps
 1040             Chem.PropertyPickleOptions.BondProps
 1041             Chem.PropertyPickleOptions.PrivateProps
 1042             Chem.PropertyPickleOptions.AllProps
 1043 
 1044     """
 1045 
 1046     return None if Mol is None else base64.b64encode(Mol.ToBinary(PropertyPickleFlags)).decode()
 1047 
 1048 
 1049 def MolFromBase64EncodedMolString(EncodedMol):
 1050     """Generate a RDKit molecule object from a base64 encoded string.
 1051 
 1052     Arguments:
 1053         str: Base64 encoded molecule string.
 1054 
 1055     Returns:
 1056         object : RDKit molecule object or None.
 1057 
 1058     """
 1059 
 1060     return None if EncodedMol is None else Chem.Mol(base64.b64decode(EncodedMol))
 1061 
 1062 
 1063 def GenerateBase64EncodedMolStrings(Mols, PropertyPickleFlags=Chem.PropertyPickleOptions.AllProps):
 1064     """Setup an iterator for generating base64 encoded molecule string
 1065     from a RDKit molecule iterator. The iterator returns a list containing
 1066     a molecule index and encoded molecule string or None.
 1067 
 1068     The molecules are pickled using RDKit Mol.ToBinary() function
 1069     before their encoding.
 1070 
 1071     Arguments:
 1072         iterator: RDKit molecules iterator.
 1073         PropertyFlags: RDKit property pickle options.
 1074 
 1075     Returns:
 1076         object : Base64 endcoded molecules iterator. The iterator returns a
 1077             list containing a molecule index and an encoded molecule string
 1078             or None.
 1079 
 1080     Notes:
 1081         The following property pickle flags are currently available in RDKit:
 1082 
 1083             Chem.PropertyPickleOptions.NoProps
 1084             Chem.PropertyPickleOptions.MolProps
 1085             Chem.PropertyPickleOptions.AtomProps
 1086             Chem.PropertyPickleOptions.BondProps
 1087             Chem.PropertyPickleOptions.PrivateProps
 1088             Chem.PropertyPickleOptions.AllProps
 1089 
 1090     Examples:
 1091 
 1092         EncodedMolsInfo = GenerateBase64EncodedMolStrings(Mols)
 1093         for MolIndex, EncodedMol in EncodedMolsInfo:
 1094             if EncodeMol is not None:
 1095                 Mol = MolFromBase64EncodedMolString(EncodedMol)
 1096 
 1097     """
 1098     for MolIndex, Mol in enumerate(Mols):
 1099         yield [MolIndex, None] if Mol is None else [MolIndex, MolToBase64EncodedMolString(Mol, PropertyPickleFlags)]
 1100 
 1101 
 1102 def GenerateBase64EncodedMolStringsWithIDs(Mols, MolIDs, PropertyPickleFlags=Chem.PropertyPickleOptions.AllProps):
 1103     """Setup an iterator for generating base64 encoded molecule string
 1104     from a RDKit molecule iterator. The iterator returns a list containing
 1105     a molecule ID and encoded molecule string or None.
 1106 
 1107     The molecules are pickled using RDKit Mol.ToBinary() function
 1108     before their encoding.
 1109 
 1110     Arguments:
 1111         iterator: RDKit molecules iterator.
 1112         MolIDs (list): Molecule IDs.
 1113         PropertyFlags: RDKit property pickle options.
 1114 
 1115     Returns:
 1116         object : Base64 endcoded molecules iterator. The iterator returns a
 1117             list containing a molecule ID and an encoded molecule string
 1118             or None.
 1119 
 1120     Notes:
 1121         The following property pickle flags are currently available in RDKit:
 1122 
 1123             Chem.PropertyPickleOptions.NoProps
 1124             Chem.PropertyPickleOptions.MolProps
 1125             Chem.PropertyPickleOptions.AtomProps
 1126             Chem.PropertyPickleOptions.BondProps
 1127             Chem.PropertyPickleOptions.PrivateProps
 1128             Chem.PropertyPickleOptions.AllProps
 1129 
 1130     Examples:
 1131 
 1132         EncodedMolsInfo = GenerateBase64EncodedMolStringsWithIDs(Mols)
 1133         for MolID, EncodedMol in EncodedMolsInfo:
 1134             if EncodeMol is not None:
 1135                 Mol = MolFromBase64EncodedMolString(EncodedMol)
 1136 
 1137     """
 1138     for MolIndex, Mol in enumerate(Mols):
 1139         yield (
 1140             [MolIDs[MolIndex], None]
 1141             if Mol is None
 1142             else [MolIDs[MolIndex], MolToBase64EncodedMolString(Mol, PropertyPickleFlags)]
 1143         )
 1144 
 1145 
 1146 def GenerateBase64EncodedMolStringWithConfIDs(
 1147     Mol, MolIndex, ConfIDs, PropertyPickleFlags=Chem.PropertyPickleOptions.AllProps
 1148 ):
 1149     """Setup an iterator generating base64 encoded molecule string for a
 1150     molecule. The iterator returns a list containing a molecule index, an encoded
 1151     molecule string, and conf ID.
 1152 
 1153     The molecules are pickled using RDKit Mol.ToBinary() function
 1154     before their encoding.
 1155 
 1156     Arguments:
 1157         Mol (object): RDKit molecule object.
 1158         MolIndex (int): Molecule index.
 1159         ConfIDs (list): Conformer IDs.
 1160         PropertyFlags: RDKit property pickle options.
 1161 
 1162     Returns:
 1163         object : Base64 endcoded molecules iterator. The iterator returns a
 1164             list containing a molecule index, an encoded molecule string, and
 1165             conf ID.
 1166 
 1167     Notes:
 1168         The following property pickle flags are currently available in RDKit:
 1169 
 1170             Chem.PropertyPickleOptions.NoProps
 1171             Chem.PropertyPickleOptions.MolProps
 1172             Chem.PropertyPickleOptions.AtomProps
 1173             Chem.PropertyPickleOptions.BondProps
 1174             Chem.PropertyPickleOptions.PrivateProps
 1175             Chem.PropertyPickleOptions.AllProps
 1176 
 1177     Examples:
 1178 
 1179         EncodedMolsInfo = GenerateBase64EncodedMolStringWithConfIDs(Mol, MolIndex, ConfIDs)
 1180         for MolIndex, EncodedMol, ConfID in EncodedMolsInfo:
 1181             if EncodeMol is not None:
 1182                 Mol = MolFromBase64EncodedMolString(EncodedMol)
 1183 
 1184     """
 1185     for ConfID in ConfIDs:
 1186         yield (
 1187             [MolIndex, None, ConfID]
 1188             if Mol is None
 1189             else [MolIndex, MolToBase64EncodedMolString(Mol, PropertyPickleFlags), ConfID]
 1190         )
 1191 
 1192 
 1193 def AreAtomMapNumbersPresentInMol(Mol):
 1194     """Check for the presence of atom map numbers in a molecue.
 1195 
 1196     Arguments:
 1197         Mol (object): RDKit molecule object.
 1198 
 1199     Returns:
 1200         bool : True - Atom map numbers present; Otherwise, false.
 1201 
 1202     """
 1203 
 1204     return False if _GetAtomMapIndices(Mol) is None else True
 1205 
 1206 
 1207 def ClearAtomMapNumbers(Mol, AllowImplicitValence=True, ClearRadicalElectrons=True):
 1208     """Check and clear atom map numbers in a molecule. In addition, allow implicit
 1209     valence and clear radical electrons for atoms with associated map numbers.
 1210 
 1211     For example, the following atomic properties are assigned by RDKit to atom
 1212     map number 1 in a molecule corresponding to SMILES C[C:1](C)C:
 1213 
 1214     NoImplicit: True; ImplicitValence: 0; ExplicitValence: 3; NumExplicitHs: 0;
 1215     NumImplicitHs: 0; NumRadicalElectrons: 1
 1216 
 1217     This function clears atoms map numbers in the molecule leading to SMILES
 1218     CC(C)C, along with optionally updating atomic properties as shown below:
 1219 
 1220     NoImplicit: False; ImplicitValence: 1; ExplicitValence: 3; NumExplicitHs: 0;
 1221     NumImplicitHs: 1; NumRadicalElectrons: 0
 1222 
 1223     Arguments:
 1224         Mol (object): RDKit molecule object.
 1225 
 1226     Returns:
 1227         Mol (object): RDKit molecule object.
 1228 
 1229     """
 1230 
 1231     AtomMapIndices = GetAtomMapIndices(Mol)
 1232 
 1233     if AtomMapIndices is None:
 1234         return Mol
 1235 
 1236     for AtomMapIndex in AtomMapIndices:
 1237         Atom = Mol.GetAtomWithIdx(AtomMapIndex)
 1238 
 1239         # Clear map number property 'molAtomMapNumber'...
 1240         Atom.SetAtomMapNum(0)
 1241 
 1242         # Allow implit valence...
 1243         if AllowImplicitValence:
 1244             Atom.SetNoImplicit(False)
 1245 
 1246         # Set number of electrons to 0...
 1247         if ClearRadicalElectrons:
 1248             Atom.SetNumRadicalElectrons(0)
 1249 
 1250         Atom.UpdatePropertyCache()
 1251 
 1252     Mol.UpdatePropertyCache()
 1253 
 1254     return Mol
 1255 
 1256 
 1257 def GetAtomMapIndices(Mol):
 1258     """Get a list of available atom indices corresponding to atom map numbers
 1259     present in a SMILES/SMARTS pattern used for creating a molecule. The list of
 1260     atom indices is sorted in ascending order by atom map numbers.
 1261 
 1262     Arguments:
 1263         Mol (object): RDKit molecule object.
 1264 
 1265     Returns:
 1266         list : List of atom indices sorted in the ascending order of atom map
 1267             numbers or None.
 1268 
 1269     """
 1270 
 1271     return _GetAtomMapIndices(Mol)
 1272 
 1273 
 1274 def GetAtomMapIndicesAndMapNumbers(Mol):
 1275     """Get lists of available atom indices and atom map numbers present in a
 1276     SMILES/SMARTS pattern used for creating a molecule. Both lists are sorted
 1277     in ascending order by atom map numbers.
 1278 
 1279     Arguments:
 1280         Mol (object): RDKit molecule object.
 1281 
 1282     Returns:
 1283         list : List of atom indices sorted in the ascending order of atom map
 1284             numbers or None.
 1285         list : List of atom map numbers sorted in the ascending order or None.
 1286 
 1287     """
 1288 
 1289     return _GetAtomMapIndicesAndMapNumbers(Mol)
 1290 
 1291 
 1292 def MolFromSubstructureMatch(Mol, PatternMol, AtomIndices, FilterByAtomMapNums=False):
 1293     """Generate a RDKit molecule object for a list of matched atom indices
 1294     present in a pattern molecule. The list of atom indices correspond to a
 1295     list retrieved by RDKit function GetSubstructureMatches using SMILES/SMARTS
 1296     pattern. The atom indices are optionally filtered by mapping atom numbers
 1297     to appropriate atom indices during the generation of the molecule.
 1298     For example: [O:1]=[S:2](=[O])[C:3][C:4].
 1299 
 1300     Arguments:
 1301         Mol (object): RDKit molecule object.
 1302         PatternMol (object): RDKit molecule object for a SMILES/SMARTS pattern.
 1303         AtomIndices (list): Atom indices.
 1304         FilterByAtomMapNums (bool): Filter matches by atom map numbers.
 1305 
 1306     Returns:
 1307         object : RDKit molecule object or None.
 1308 
 1309     """
 1310 
 1311     AtomMapIndices = _GetAtomMapIndices(PatternMol) if FilterByAtomMapNums else None
 1312 
 1313     return _MolFromSubstructureMatch(Mol, PatternMol, AtomIndices, AtomMapIndices)
 1314 
 1315 
 1316 def MolsFromSubstructureMatches(Mol, PatternMol, AtomIndicesList, FilterByAtomMapNums=False):
 1317     """Generate  a list of RDKit molecule objects for a list containing lists of
 1318     matched atom indices present in a pattern molecule. The list of atom indices
 1319     correspond to a list retrieved by RDKit function GetSubstructureMatches using
 1320     SMILES/SMARTS pattern. The atom indices are optionally filtered by mapping
 1321     atom numbers to appropriate atom indices during the generation of the
 1322     molecule. For example: [O:1]=[S:2](=[O])[C:3][C:4].
 1323 
 1324     Arguments:
 1325         Mol (object): RDKit molecule object.
 1326         PatternMol (object): RDKit molecule object for a SMILES/SMARTS pattern.
 1327         AtomIndicesList (list): A list of lists containing atom indices.
 1328         FilterByAtomMapNums (bool): Filter matches by atom map numbers.
 1329 
 1330     Returns:
 1331         list : A list of lists containg RDKit molecule objects or None.
 1332 
 1333     """
 1334 
 1335     AtomMapIndices = _GetAtomMapIndices(PatternMol) if FilterByAtomMapNums else None
 1336 
 1337     Mols = []
 1338     for AtomIndices in AtomIndicesList:
 1339         Mols.append(_MolFromSubstructureMatch(Mol, PatternMol, AtomIndices, AtomMapIndices))
 1340 
 1341     return Mols if len(Mols) else None
 1342 
 1343 
 1344 def FilterSubstructureMatchByAtomMapNumbers(Mol, PatternMol, AtomIndices):
 1345     """Filter a list of matched atom indices by map atom numbers present in a
 1346     pattern molecule. The list of atom indices correspond to a list retrieved by
 1347     RDKit function GetSubstructureMatches using SMILES/SMARTS pattern. The
 1348     atom map numbers are mapped to appropriate atom indices during the generation
 1349     of molecules. For example: [O:1]=[S:2](=[O])[C:3][C:4].
 1350 
 1351     Arguments:
 1352         Mol (object): RDKit molecule object.
 1353         PatternMol (object): RDKit molecule object for a SMILES/SMARTS pattern.
 1354         AtomIndices (list): Atom indices.
 1355 
 1356     Returns:
 1357         list : A list of filtered atom indices.
 1358 
 1359     """
 1360     AtomMapIndices = _GetAtomMapIndices(PatternMol)
 1361 
 1362     return _FilterSubstructureMatchByAtomMapNumbers(Mol, PatternMol, AtomIndices, AtomMapIndices)
 1363 
 1364 
 1365 def FilterSubstructureMatchesByAtomMapNumbers(Mol, PatternMol, AtomIndicesList):
 1366     """Filter a list of lists containing matched atom indices by map atom numbers
 1367     present in a pattern molecule. The list of atom indices correspond to a list retrieved by
 1368     RDKit function GetSubstructureMatches using SMILES/SMARTS pattern. The
 1369     atom map numbers are mapped to appropriate atom indices during the generation
 1370     of molecules. For example: [O:1]=[S:2](=[O])[C:3][C:4].
 1371 
 1372     Arguments:
 1373         Mol (object): RDKit molecule object.
 1374         PatternMol (object): RDKit molecule object for a SMILES/SMARTS pattern.
 1375         AtomIndicesList (list): A list of lists containing atom indices.
 1376 
 1377     Returns:
 1378         list : A list of lists containing filtered atom indices.
 1379 
 1380     """
 1381     AtomMapIndices = _GetAtomMapIndices(PatternMol)
 1382 
 1383     MatchedAtomIndicesList = []
 1384     for AtomIndices in AtomIndicesList:
 1385         MatchedAtomIndicesList.append(
 1386             _FilterSubstructureMatchByAtomMapNumbers(Mol, PatternMol, AtomIndices, AtomMapIndices)
 1387         )
 1388 
 1389     return MatchedAtomIndicesList
 1390 
 1391 
 1392 def _MolFromSubstructureMatch(Mol, PatternMol, AtomIndices, AtomMapIndices):
 1393     """Generate a RDKit molecule object for a list of matched atom indices and available
 1394     atom map indices.
 1395     """
 1396 
 1397     if AtomMapIndices is not None:
 1398         MatchedAtomIndices = [AtomIndices[Index] for Index in AtomMapIndices]
 1399     else:
 1400         MatchedAtomIndices = list(AtomIndices)
 1401 
 1402     return _GetMolFromAtomIndices(Mol, MatchedAtomIndices)
 1403 
 1404 
 1405 def _GetAtomMapIndices(Mol):
 1406     """Get a list of available atom indices corresponding to sorted atom map
 1407     numbers present in a SMILES/SMARTS pattern used for creating a molecule.
 1408     """
 1409 
 1410     AtomMapIndices, AtomMapNumbers = _GetAtomMapIndicesAndMapNumbers(Mol)
 1411 
 1412     return AtomMapIndices
 1413 
 1414 
 1415 def _GetAtomMapIndicesAndMapNumbers(Mol):
 1416     """Get a list of available atom indices and atom map numbers present
 1417     in  a SMILES/SMARTS pattern used for creating a molecule. Both lists
 1418     are sorted in ascending order by atom map numbers.
 1419     """
 1420 
 1421     # Setup a atom map number to atom indices map..
 1422     AtomMapNumToIndices = {}
 1423     for Atom in Mol.GetAtoms():
 1424         AtomMapNum = Atom.GetAtomMapNum()
 1425 
 1426         if AtomMapNum:
 1427             AtomMapNumToIndices[AtomMapNum] = Atom.GetIdx()
 1428 
 1429     # Setup atom indices corresponding to sorted atom map numbers...
 1430     AtomMapIndices = None
 1431     AtomMapNumbers = None
 1432     if len(AtomMapNumToIndices):
 1433         AtomMapNumbers = sorted(AtomMapNumToIndices)
 1434         AtomMapIndices = [AtomMapNumToIndices[AtomMapNum] for AtomMapNum in AtomMapNumbers]
 1435 
 1436     return (AtomMapIndices, AtomMapNumbers)
 1437 
 1438 
 1439 def _FilterSubstructureMatchByAtomMapNumbers(Mol, PatternMol, AtomIndices, AtomMapIndices):
 1440     """Filter substructure match atom indices by atom map indices corresponding to
 1441     atom map numbers.
 1442     """
 1443 
 1444     if AtomMapIndices is None:
 1445         return list(AtomIndices)
 1446 
 1447     return [AtomIndices[Index] for Index in AtomMapIndices]
 1448 
 1449 
 1450 def _GetMolFromAtomIndices(Mol, AtomIndices):
 1451     """Generate a RDKit molecule object from atom indices returned by
 1452     substructure search.
 1453     """
 1454 
 1455     BondIndices = []
 1456     for AtomIndex in AtomIndices:
 1457         Atom = Mol.GetAtomWithIdx(AtomIndex)
 1458 
 1459         for AtomNbr in Atom.GetNeighbors():
 1460             AtomNbrIndex = AtomNbr.GetIdx()
 1461             if AtomNbrIndex not in AtomIndices:
 1462                 continue
 1463 
 1464             BondIndex = Mol.GetBondBetweenAtoms(AtomIndex, AtomNbrIndex).GetIdx()
 1465             if BondIndex in BondIndices:
 1466                 continue
 1467 
 1468             BondIndices.append(BondIndex)
 1469 
 1470     MatchedMol = Chem.PathToSubmol(Mol, BondIndices) if len(BondIndices) else None
 1471 
 1472     return MatchedMol
 1473 
 1474 
 1475 def ConstrainAndEmbed(
 1476     mol,
 1477     core,
 1478     coreMatchesMol=None,
 1479     useTethers=True,
 1480     coreConfId=-1,
 1481     randomseed=2342,
 1482     getForceField=AllChem.UFFGetMoleculeForceField,
 1483     **kwargs,
 1484 ):
 1485     """
 1486     The function is a local copy of RDKit fucntion AllChem.ConstrainedEmbed().
 1487     It has been enhanced to support an explicit list of core matches corresponding
 1488     to the matched atom indices in the molecule. The number of matched atom indices
 1489     must be equal to the number of atoms in core molecule.
 1490 
 1491     Arguments:
 1492         mol (object): RDKit molecule object to embed.
 1493         core (object): RDKit molecule to use as a source of constraints.
 1494         coreMatchesMol (list): A list matches atom indices in mol.
 1495         useTethers: (bool) if True, the final conformation will be optimized
 1496             subject to a series of extra forces that pull the matching atoms to
 1497             the positions of the core atoms. Otherwise simple distance
 1498             constraints based on the core atoms will be used in the
 1499             optimization.
 1500         coreConfId (int): ID of the core conformation to use.
 1501         randomSeed (int): Seed for the random number generator
 1502 
 1503     Returns:
 1504         mol (object): RDKit molecule object.
 1505 
 1506     """
 1507     if coreMatchesMol is None:
 1508         match = mol.GetSubstructMatch(core)
 1509         if not match:
 1510             raise ValueError("Molecule doesn't match the core.")
 1511     else:
 1512         if core.GetNumAtoms() != len(coreMatchesMol):
 1513             raise ValueError(
 1514                 "Number of atoms, %s, in core molecule must match number of atom indices, %s, specified in the list coreMatchesMol."
 1515                 % (core.GetNumAtoms(), len(coreMatchesMol))
 1516             )
 1517         # Check specified matched atom indices in  coreMatchesMol and use the match atom
 1518         # indices returned by GetSubstructMatches() for embedding...
 1519         coreMatch = None
 1520         matches = mol.GetSubstructMatches(core)
 1521         for match in matches:
 1522             if len(match) != len(coreMatchesMol):
 1523                 continue
 1524             matchFound = True
 1525             for atomIndex in match:
 1526                 if atomIndex not in coreMatchesMol:
 1527                     matchFound = False
 1528                     break
 1529             if matchFound:
 1530                 coreMatch = match
 1531                 break
 1532         if coreMatch is None:
 1533             raise ValueError("Molecule doesn't match the atom indices specified in the list coreMatchesMol.")
 1534         match = coreMatch
 1535 
 1536     coordMap = {}
 1537     coreConf = core.GetConformer(coreConfId)
 1538     for i, idxI in enumerate(match):
 1539         corePtI = coreConf.GetAtomPosition(i)
 1540         coordMap[idxI] = corePtI
 1541 
 1542     ci = AllChem.EmbedMolecule(mol, coordMap=coordMap, randomSeed=randomseed, **kwargs)
 1543     if ci < 0:
 1544         raise ValueError("Could not embed molecule.")
 1545 
 1546     algMap = [(j, i) for i, j in enumerate(match)]
 1547 
 1548     if not useTethers:
 1549         # clean up the conformation
 1550         ff = getForceField(mol, confId=0)
 1551         for i, idxI in enumerate(match):
 1552             for j in range(i + 1, len(match)):
 1553                 idxJ = match[j]
 1554                 d = coordMap[idxI].Distance(coordMap[idxJ])
 1555                 ff.AddDistanceConstraint(idxI, idxJ, d, d, 100.0)
 1556         ff.Initialize()
 1557         n = 4
 1558         more = ff.Minimize()
 1559         while more and n:
 1560             more = ff.Minimize()
 1561             n -= 1
 1562         # rotate the embedded conformation onto the core:
 1563         rms = AllChem.AlignMol(mol, core, atomMap=algMap)
 1564     else:
 1565         # rotate the embedded conformation onto the core:
 1566         rms = AllChem.AlignMol(mol, core, atomMap=algMap)
 1567         ff = getForceField(mol, confId=0)
 1568         conf = core.GetConformer()
 1569         for i in range(core.GetNumAtoms()):
 1570             p = conf.GetAtomPosition(i)
 1571             pIdx = ff.AddExtraPoint(p.x, p.y, p.z, fixed=True) - 1
 1572             ff.AddDistanceConstraint(pIdx, match[i], 0, 0, 100.0)
 1573         ff.Initialize()
 1574         n = 4
 1575         more = ff.Minimize(energyTol=1e-4, forceTol=1e-3)
 1576         while more and n:
 1577             more = ff.Minimize(energyTol=1e-4, forceTol=1e-3)
 1578             n -= 1
 1579         # realign
 1580         rms = AllChem.AlignMol(mol, core, atomMap=algMap)
 1581 
 1582     mol.SetProp("EmbedRMS", str(rms))
 1583     return mol
 1584 
 1585 
 1586 def ReadAndValidateMolecules(FileName, **KeyWordArgs):
 1587     """Read molecules from an input file, validate all molecule objects, and return
 1588     a list of valid molecule objects along with the count of valid and non-valid molecule
 1589     objects.
 1590 
 1591     Arguments:
 1592         FileName (str): Name of a file with complete path.
 1593         **KeyWordArgs (dictionary) : Parameter name and value pairs for reading and
 1594             processing molecules.
 1595 
 1596     Returns:
 1597         list : List of valid RDKit molecule objects.
 1598         int : Number of total molecules in input file.
 1599         int : Number of valid molecules in input file.
 1600 
 1601     Notes:
 1602         The file extension is used to determine type of the file and set up an appropriate
 1603         file reader.
 1604 
 1605     """
 1606 
 1607     AllowEmptyMols = True
 1608     if "AllowEmptyMols" in KeyWordArgs:
 1609         AllowEmptyMols = KeyWordArgs["AllowEmptyMols"]
 1610 
 1611     Mols = ReadMolecules(FileName, **KeyWordArgs)
 1612 
 1613     if AllowEmptyMols:
 1614         ValidMols = [Mol for Mol in Mols if Mol is not None]
 1615     else:
 1616         ValidMols = []
 1617         MolCount = 0
 1618         for Mol in Mols:
 1619             MolCount += 1
 1620             if Mol is None:
 1621                 continue
 1622 
 1623             if IsMolEmpty(Mol):
 1624                 MolName = GetMolName(Mol, MolCount)
 1625                 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 1626                 continue
 1627 
 1628             ValidMols.append(Mol)
 1629 
 1630     MolCount = len(Mols)
 1631     ValidMolCount = len(ValidMols)
 1632 
 1633     return (ValidMols, MolCount, ValidMolCount)
 1634 
 1635 
 1636 def ReadMolecules(FileName, **KeyWordArgs):
 1637     """Read molecules from an input file without performing any validation
 1638     and creation of molecule objects.
 1639 
 1640     Arguments:
 1641         FileName (str): Name of a file with complete path.
 1642         **KeyWordArgs (dictionary) : Parameter name and value pairs for reading and
 1643             processing molecules.
 1644 
 1645     Returns:
 1646         list : List of RDKit molecule objects.
 1647 
 1648     Notes:
 1649         The file extension is used to determine type of the file and set up an appropriate
 1650         file reader.
 1651 
 1652     """
 1653 
 1654     # Set default values for possible arguments...
 1655     ReaderArgs = {
 1656         "Sanitize": True,
 1657         "RemoveHydrogens": True,
 1658         "StrictParsing": True,
 1659         "SMILESDelimiter": " ",
 1660         "SMILESColumn": 1,
 1661         "SMILESNameColumn": 2,
 1662         "SMILESTitleLine": True,
 1663     }
 1664 
 1665     # Set specified values for possible arguments...
 1666     for Arg in ReaderArgs:
 1667         if Arg in KeyWordArgs:
 1668             ReaderArgs[Arg] = KeyWordArgs[Arg]
 1669 
 1670     # Modify specific valeus for SMILES...
 1671     if MiscUtil.CheckFileExt(FileName, "smi csv tsv txt"):
 1672         Args = ["Sanitize", "SMILESTitleLine"]
 1673         for Arg in Args:
 1674             if ReaderArgs[Arg] is True:
 1675                 ReaderArgs[Arg] = 1
 1676             else:
 1677                 ReaderArgs[Arg] = 0
 1678 
 1679     Mols = []
 1680     if MiscUtil.CheckFileExt(FileName, "sdf sd"):
 1681         return ReadMoleculesFromSDFile(
 1682             FileName, ReaderArgs["Sanitize"], ReaderArgs["RemoveHydrogens"], ReaderArgs["StrictParsing"]
 1683         )
 1684     elif MiscUtil.CheckFileExt(FileName, "mol"):
 1685         return ReadMoleculesFromMolFile(
 1686             FileName, ReaderArgs["Sanitize"], ReaderArgs["RemoveHydrogens"], ReaderArgs["StrictParsing"]
 1687         )
 1688     elif MiscUtil.CheckFileExt(FileName, "mol2"):
 1689         return ReadMoleculesFromMol2File(FileName, ReaderArgs["Sanitize"], ReaderArgs["RemoveHydrogens"])
 1690     elif MiscUtil.CheckFileExt(FileName, "pdb"):
 1691         return ReadMoleculesFromPDBFile(FileName, ReaderArgs["Sanitize"], ReaderArgs["RemoveHydrogens"])
 1692     elif MiscUtil.CheckFileExt(FileName, "smi txt csv tsv"):
 1693         SMILESColumnIndex = ReaderArgs["SMILESColumn"] - 1
 1694         SMILESNameColumnIndex = ReaderArgs["SMILESNameColumn"] - 1
 1695         return ReadMoleculesFromSMILESFile(
 1696             FileName,
 1697             ReaderArgs["SMILESDelimiter"],
 1698             SMILESColumnIndex,
 1699             SMILESNameColumnIndex,
 1700             ReaderArgs["SMILESTitleLine"],
 1701             ReaderArgs["Sanitize"],
 1702         )
 1703     else:
 1704         MiscUtil.PrintWarning("RDKitUtil.ReadMolecules: Non supported file type: %s" % FileName)
 1705 
 1706     return Mols
 1707 
 1708 
 1709 def ReadMoleculesFromSDFile(FileName, Sanitize=True, RemoveHydrogens=True, StrictParsing=True):
 1710     """Read molecules from a SD file.
 1711 
 1712     Arguments:
 1713         FileName (str): Name of a file with complete path.
 1714         Sanitize (bool): Sanitize molecules.
 1715         RemoveHydrogens (bool): Remove hydrogens from molecules.
 1716         StrictParsing (bool): Perform strict parsing.
 1717 
 1718     Returns:
 1719         list : List of RDKit molecule objects.
 1720 
 1721     """
 1722     return Chem.SDMolSupplier(FileName, sanitize=Sanitize, removeHs=RemoveHydrogens, strictParsing=StrictParsing)
 1723 
 1724 
 1725 def ReadMoleculesFromMolFile(FileName, Sanitize=True, RemoveHydrogens=True, StrictParsing=True):
 1726     """Read molecule from a MDL Mol file.
 1727 
 1728     Arguments:
 1729         FileName (str): Name of a file with complete path.
 1730         Sanitize (bool): Sanitize molecules.
 1731         RemoveHydrogens (bool): Remove hydrogens from molecules.
 1732         StrictParsing (bool): Perform strict parsing.
 1733 
 1734     Returns:
 1735         list : List of RDKit molecule objects.
 1736 
 1737     """
 1738 
 1739     Mols = []
 1740     Mols.append(Chem.MolFromMolFile(FileName, sanitize=Sanitize, removeHs=RemoveHydrogens, strictParsing=StrictParsing))
 1741     return Mols
 1742 
 1743 
 1744 def ReadMoleculesFromMol2File(FileName, Sanitize=True, RemoveHydrogens=True):
 1745     """Read molecules from a Tripos Mol2  file. The first call to the function
 1746     creates and returns  a generator object using Python yield statement. The
 1747     molecules are created during the subsequent iteration by the generator object.
 1748 
 1749     Arguments:
 1750         FileName (str): Name of a file with complete path.
 1751         Sanitize (bool): Sanitize molecules.
 1752         RemoveHydrogens (bool): Remove hydrogens from molecules.
 1753 
 1754     Returns:
 1755         list : A Python generator object for iterating over the molecules.
 1756 
 1757     """
 1758 
 1759     return _Mol2MolSupplier(FileName, Sanitize, RemoveHydrogens)
 1760 
 1761 
 1762 def _Mol2MolSupplier(FileName, Sanitize=True, RemoveHydrogens=True):
 1763     """Read molecules from a Tripos Mol2  file."""
 1764 
 1765     fh = open(FileName, "r")
 1766 
 1767     FirstMol = True
 1768     ProcessingMol = False
 1769 
 1770     for Line in fh:
 1771         if re.match("^#", Line, re.I):
 1772             continue
 1773 
 1774         if re.match("^@<TRIPOS>MOLECULE", Line, re.I):
 1775             ProcessingMol = True
 1776 
 1777             if FirstMol:
 1778                 FirstMol = False
 1779 
 1780                 MolLines = []
 1781                 MolLines.append(Line)
 1782                 continue
 1783 
 1784             # Process lines for existing molecule...
 1785             MolBlock = "".join(MolLines)
 1786 
 1787             Mol = Chem.MolFromMol2Block(MolBlock, sanitize=Sanitize, removeHs=RemoveHydrogens)
 1788             yield Mol
 1789 
 1790             # Track lines for next molecule...
 1791             MolLines = []
 1792             MolLines.append(Line)
 1793             continue
 1794 
 1795         if not ProcessingMol:
 1796             continue
 1797 
 1798         MolLines.append(Line)
 1799 
 1800     fh.close
 1801 
 1802     # Process last molecule...
 1803     if len(MolLines):
 1804         MolBlock = "".join(MolLines)
 1805         Mol = Chem.MolFromMol2Block(MolBlock, sanitize=Sanitize, removeHs=RemoveHydrogens)
 1806         yield Mol
 1807 
 1808 
 1809 def ReadMoleculesFromPDBFile(FileName, Sanitize=True, RemoveHydrogens=True):
 1810     """Read molecule from a PDB  file.
 1811 
 1812     Arguments:
 1813         FileName (str): Name of a file with complete path.
 1814         Sanitize (bool): Sanitize molecules.
 1815         RemoveHydrogens (bool): Remove hydrogens from molecules.
 1816 
 1817     Returns:
 1818         list : List of RDKit molecule objects.
 1819 
 1820     """
 1821 
 1822     Mols = []
 1823     Mols.append(Chem.MolFromPDBFile(FileName, sanitize=Sanitize, removeHs=RemoveHydrogens))
 1824     return Mols
 1825 
 1826 
 1827 def ReadMoleculesFromSMILESFile(
 1828     FileName, SMILESDelimiter=" ", SMILESColIndex=0, SMILESNameColIndex=1, SMILESTitleLine=1, Sanitize=1
 1829 ):
 1830     """Read molecules from a SMILES file.
 1831 
 1832     Arguments:
 1833         SMILESDelimiter (str): Delimiter for parsing SMILES line
 1834         SMILESColIndex (int): Column index containing SMILES string.
 1835         SMILESNameColIndex (int): Column index containing molecule name.
 1836         SMILESTitleLine (int): Flag to indicate presence of title line.
 1837         Sanitize (int): Sanitize molecules.
 1838 
 1839     Returns:
 1840         list : List of RDKit molecule objects.
 1841 
 1842     """
 1843 
 1844     return Chem.SmilesMolSupplier(
 1845         FileName,
 1846         delimiter=SMILESDelimiter,
 1847         smilesColumn=SMILESColIndex,
 1848         nameColumn=SMILESNameColIndex,
 1849         titleLine=SMILESTitleLine,
 1850         sanitize=Sanitize,
 1851     )
 1852 
 1853 
 1854 def MoleculesWriter(FileName, **KeyWordArgs):
 1855     """Set up a molecule writer.
 1856 
 1857     Arguments:
 1858         FileName (str): Name of a file with complete path.
 1859         **KeyWordArgs (dictionary) : Parameter name and value pairs for writing and
 1860             processing molecules.
 1861 
 1862     Returns:
 1863         RDKit object : Molecule writer.
 1864 
 1865     Notes:
 1866         The file extension is used to determine type of the file and set up an appropriate
 1867         file writer.
 1868 
 1869     """
 1870 
 1871     # Set default values for possible arguments...
 1872     WriterArgs = {
 1873         "Compute2DCoords": False,
 1874         "Kekulize": True,
 1875         "ForceV3000": False,
 1876         "SMILESKekulize": False,
 1877         "SMILESDelimiter": " ",
 1878         "SMILESIsomeric": True,
 1879         "SMILESTitleLine": True,
 1880         "SMILESMolName": True,
 1881     }
 1882 
 1883     # Set specified values for possible arguments...
 1884     for Arg in WriterArgs:
 1885         if Arg in KeyWordArgs:
 1886             WriterArgs[Arg] = KeyWordArgs[Arg]
 1887 
 1888     Writer = None
 1889     if MiscUtil.CheckFileExt(FileName, "sdf sd"):
 1890         Writer = Chem.SDWriter(FileName)
 1891         Writer.SetKekulize(WriterArgs["Kekulize"])
 1892         Writer.SetForceV3000(WriterArgs["ForceV3000"])
 1893     elif MiscUtil.CheckFileExt(FileName, "pdb"):
 1894         Writer = Chem.PDBWriter(FileName)
 1895     elif MiscUtil.CheckFileExt(FileName, "smi"):
 1896         # Text for the name column in the title line. Blank indicates not to include name column
 1897         # in the output file...
 1898         NameHeader = "Name" if WriterArgs["SMILESMolName"] else ""
 1899         Writer = Chem.SmilesWriter(
 1900             FileName,
 1901             delimiter=WriterArgs["SMILESDelimiter"],
 1902             nameHeader=NameHeader,
 1903             includeHeader=WriterArgs["SMILESTitleLine"],
 1904             isomericSmiles=WriterArgs["SMILESIsomeric"],
 1905             kekuleSmiles=WriterArgs["SMILESKekulize"],
 1906         )
 1907     else:
 1908         MiscUtil.PrintWarning("RDKitUtil.WriteMolecules: Non supported file type: %s" % FileName)
 1909 
 1910     return Writer
 1911 
 1912 
 1913 def WriteMolecules(FileName, Mols, **KeyWordArgs):
 1914     """Write molecules to an output file.
 1915 
 1916     Arguments:
 1917         FileName (str): Name of a file with complete path.
 1918         Mols (list): List of RDKit molecule objects.
 1919         **KeyWordArgs (dictionary) : Parameter name and value pairs for writing and
 1920             processing molecules.
 1921 
 1922     Returns:
 1923         int : Number of total molecules.
 1924         int : Number of processed molecules written to output file.
 1925 
 1926     Notes:
 1927         The file extension is used to determine type of the file and set up an appropriate
 1928         file writer.
 1929 
 1930     """
 1931 
 1932     Compute2DCoords = False
 1933     if "Compute2DCoords" in KeyWordArgs:
 1934         Compute2DCoords = KeyWordArgs["Compute2DCoords"]
 1935 
 1936     SetSMILESMolProps = KeyWordArgs["SetSMILESMolProps"] if "SetSMILESMolProps" in KeyWordArgs else False
 1937 
 1938     MolCount = 0
 1939     ProcessedMolCount = 0
 1940 
 1941     Writer = MoleculesWriter(FileName, **KeyWordArgs)
 1942 
 1943     if Writer is None:
 1944         return (MolCount, ProcessedMolCount)
 1945 
 1946     FirstMol = True
 1947     for Mol in Mols:
 1948         MolCount += 1
 1949         if Mol is None:
 1950             continue
 1951 
 1952         if FirstMol:
 1953             FirstMol = False
 1954             if SetSMILESMolProps:
 1955                 SetWriterMolProps(Writer, Mol)
 1956 
 1957         ProcessedMolCount += 1
 1958         if Compute2DCoords:
 1959             AllChem.Compute2DCoords(Mol)
 1960 
 1961         Writer.write(Mol)
 1962 
 1963     Writer.close()
 1964 
 1965     return (MolCount, ProcessedMolCount)
 1966 
 1967 
 1968 def SetWriterMolProps(Writer, Mol):
 1969     """Setup molecule properties for a writer to output.
 1970 
 1971     Arguments:
 1972         Writer (object): RDKit writer object.
 1973         Mol (object): RDKit molecule object.
 1974 
 1975     Returns:
 1976         object : Writer object.
 1977 
 1978     """
 1979     PropNames = list(Mol.GetPropNames())
 1980     if len(PropNames):
 1981         Writer.SetProps(PropNames)
 1982 
 1983     return Writer
 1984 
 1985 
 1986 def SetupHTMLForTorsionScanViewer(
 1987     MolName, TorsionMols, TorsionEnergies, RelativeTorsionEnergies, TorsionAngles, Units, PlotHeight, TitleLine
 1988 ):
 1989     """Setup HTML  for torsion scan viewer for writing to a text file. The viewer
 1990     consists of plot and structure viewer panels. The plot panel contains an
 1991     interactive relative energy rendered using Plotly. The structure of the
 1992     molecules, corresponding to the scanned torsion angles, are shown in the
 1993     structure panel employing 3DMol.js. In addition, an animation button is
 1994     available to automatically scan the torsion angles and view the results.
 1995 
 1996     Arguments:
 1997         MolName (str): Molecule name.
 1998         TorsionEnergies (list): List of energies for scanned torsion angles.
 1999         RelativeTorsionEnergies (list): List of relative energies for scanned
 2000             torsion angles.
 2001         TorsionAngles (list): List of scanned torsion angles.
 2002         Units (str): Energy units.
 2003         PlotHeight (int): Energy plot height in pixels.
 2004         TitleLine (str): Title line torsion scan viewer.
 2005 
 2006     Returns:
 2007         str: HTML text for torsion scan viewer.
 2008 
 2009     """
 2010     TorsionDataFrames = _SetupTorsionScanViewerDataFrames(
 2011         MolName, TorsionMols, TorsionEnergies, RelativeTorsionEnergies, TorsionAngles
 2012     )
 2013 
 2014     # Setup PlotHeight and strucure viewer minimum height...
 2015     StrViewerMinHeight = PlotHeight - 90
 2016     if StrViewerMinHeight <= 0:
 2017         StrViewerMinHeight = PlotHeight
 2018 
 2019     PlotHeight = "%spx" % PlotHeight
 2020     StrViewerMinHeight = "%spx" % StrViewerMinHeight
 2021 
 2022     # Setup footer line...
 2023     FooterLine = "Visualization: Plotly &middot; 3Dmol.js&semi; Calculations: RDKit &middot; MayaChemTools"
 2024 
 2025     # Setup a JSON data string...
 2026     TorsionDataFramesJSONString = json.dumps(TorsionDataFrames, separators=(",", ":"))
 2027 
 2028     # Setup viewer HTML template string...
 2029     ViewerTemplateHTML = _SetupHTMLTorsionScanViewerTemplate()
 2030 
 2031     # Update viewer HTML template string...
 2032     ViewerHTML = ViewerTemplateHTML.replace("__DATA__", TorsionDataFramesJSONString)
 2033 
 2034     ViewerHTML = ViewerHTML.replace("__TITLE__", TitleLine).replace("__FOOTER__", FooterLine)
 2035     ViewerHTML = ViewerHTML.replace("__UNITS__", Units)
 2036     ViewerHTML = ViewerHTML.replace("__PLOT_HEIGHT__", PlotHeight).replace("__VIEWER_MIN_HEIGHT__", StrViewerMinHeight)
 2037 
 2038     return ViewerHTML
 2039 
 2040 
 2041 def _SetupTorsionScanViewerDataFrames(MolName, TorsionMols, TorsionEnergies, RelativeTorsionEnergies, TorsionAngles):
 2042     """Setup torsion scan viewer data frames."""
 2043 
 2044     TorsionDataFrames = []
 2045 
 2046     for Index, TorsionMol in enumerate(TorsionMols):
 2047         TorsionAngle = "%s" % TorsionAngles[Index]
 2048 
 2049         TorsionMolName = "%s_Deg%s" % (MolName, TorsionAngle)
 2050         TorsionMol.SetProp("_Name", TorsionMolName)
 2051 
 2052         # Setup molblock with standardized new lines...
 2053         TorsionMolBlock = Chem.MolToMolBlock(TorsionMol)
 2054         TorsionMolBlock = re.sub(r"(\r\n|\r)", r"\n", TorsionMolBlock)
 2055 
 2056         TorsionEnergy = "%.2f" % TorsionEnergies[Index]
 2057         RelativeTorsionEnergy = "%.2f" % RelativeTorsionEnergies[Index]
 2058 
 2059         TorsionDataFrames.append(
 2060             {
 2061                 "title": TorsionMolName,
 2062                 "angle": float(TorsionAngle),
 2063                 "energy": float(TorsionEnergy),
 2064                 "relEnergy": float(RelativeTorsionEnergy),
 2065                 "mol": TorsionMolBlock,
 2066             }
 2067         )
 2068 
 2069     return TorsionDataFrames
 2070 
 2071 
 2072 def _SetupHTMLTorsionScanViewerTemplate():
 2073     """Setup torsion scan viewer template."""
 2074 
 2075     ViewerTemplate = r"""<!DOCTYPE html>
 2076 <html lang="en">
 2077 <head>
 2078 <meta charset="utf-8">
 2079 <meta name="viewport" content="width=device-width, initial-scale=1">
 2080 <title>Torsion Profile Viewer</title>
 2081 <script src="https://cdn.plot.ly/plotly-2.35.2.min.js" charset="utf-8"></script>
 2082 <script src="https://3Dmol.org/build/3Dmol-min.js"></script>
 2083 <style>
 2084   :root { --bg:#f7f7fa; --panel:#fff; --line:#4c72b0; --accent:#dd6b3f; --ink:#262626; }
 2085   * { box-sizing: border-box; }
 2086   body { margin:0; font-family:sans-serif,Helvetica,Arial;
 2087          background:var(--bg); color:var(--ink); }
 2088   header { padding:14px 22px 6px; }
 2089   header h1 { margin:0; font-size:18px; font-weight:700; }
 2090   header p { margin:4px 0 0; font-size:13px; color:#666; }
 2091   .wrap { display:flex; flex-wrap:wrap; gap:16px; padding:14px 22px 22px; align-items:stretch; }
 2092 
 2093   .panel { background:var(--panel); border:1px solid #e3e3ea; border-radius:10px;
 2094            box-shadow:0 1px 3px rgba(0,0,0,.05); }
 2095   #plotPanel { flex:1 1 460px; min-width:380px; padding:8px; }
 2096   #plot { width:100%; height:__PLOT_HEIGHT__; }
 2097   #structPanel { flex:1 1 420px; min-width:340px; display:flex; flex-direction:column; padding:12px; }
 2098   #viewerHolder { position:relative; flex:1 1 auto; min-height:__VIEWER_MIN_HEIGHT__; border-radius:8px;
 2099                   overflow:hidden; background:#fdfdfd; border:1px solid #eee; }
 2100   #viewer { position:absolute; inset:0; }
 2101 
 2102   .readout { display:flex; gap:18px; flex-wrap:wrap; margin-top:10px; font-size:13px; }
 2103   .readout b { font-size:15px; font-weight:700; display:block; }
 2104   .readout .lab { color:#888; font-size:12px; letter-spacing:.04em; }
 2105 
 2106   .controls { display:flex; align-items:center; gap:12px; margin-top:12px; }
 2107   .controls button { font-size:15px; border:1px solid #ccd; background:#fff; border-radius:7px;
 2108                      padding:6px 14px; cursor:pointer; }
 2109   .controls button:hover { background:#eef; }
 2110 
 2111   #slider { flex:1 1 auto; }
 2112 
 2113   .opts { display:flex; gap:16px; align-items:center; margin-top:6px; font-size:12.5px; color:#555; }
 2114   .opts label { cursor:pointer; }
 2115 
 2116   footer { padding:0 22px 18px; font-size:11.5px; color:#999; }
 2117 </style>
 2118 </head>
 2119 <body>
 2120 <header>
 2121   <h1>Torsional Energy Profile</h1>
 2122   <p>__TITLE__ </p>
 2123 </header>
 2124 
 2125 <div class="wrap">
 2126   <div class="panel" id="plotPanel"><div id="plot"></div></div>
 2127 
 2128   <div class="panel" id="structPanel">
 2129     <div id="viewerHolder"><div id="viewer"></div></div>
 2130     <div class="readout">
 2131       <div><span class="lab">Torsion Angle</span><b><span id="rAngle">&ndash;</span>&deg;</b></div>
 2132       <div><span class="lab">Relative Energy</span><b><span id="rRelEnergy">&ndash;</span></b><span class="lab">__UNITS__</span></div>
 2133       <div><span class="lab">Energy</span><b><span id="rAbs">&ndash;</span></b><span class="lab">__UNITS__</span></div>
 2134     </div>
 2135     <div class="controls">
 2136       <button id="play">&#9654; Play</button>
 2137       <input type="range" id="slider" min="0" max="0" value="0" step="1">
 2138     </div>
 2139     <div class="opts">
 2140       <label><input type="checkbox" id="optSticks" checked> Sticks</label>
 2141       <label><input type="checkbox" id="optH"> Show H</label>
 2142       <label><input type="checkbox" id="optSpin"> Spin</label>
 2143       <label><input type="checkbox" id="optFreeze"> Lock view while scanning</label>
 2144     </div>
 2145   </div>
 2146 </div>
 2147 <footer>__FOOTER__</footer>
 2148 
 2149 <script>
 2150 const FRAMES = __DATA__;
 2151 const angles = FRAMES.map(f => f.angle);
 2152 const relEnergy    = FRAMES.map(f => f.relEnergy);
 2153 
 2154 let current = 0;
 2155 let playing = false;
 2156 let timer = null;
 2157 
 2158 const viewer = $3Dmol.createViewer("viewer", { backgroundColor: "white" });
 2159 let model = null;
 2160 
 2161 function renderStructure(idx, keepView) {
 2162   const f = FRAMES[idx];
 2163   const showH = document.getElementById("optH").checked;
 2164 
 2165   viewer.removeAllModels();
 2166 
 2167   model = viewer.addModel(f.mol, "sdf");
 2168   const sticks = document.getElementById("optSticks").checked;
 2169   const base = sticks
 2170     ? { stick: { radius: 0.20 }, sphere: { scale: 0.0 } }
 2171     : { stick: { radius: 0.20 }, sphere: { scale: 0.30 } };
 2172   viewer.setStyle({}, base);
 2173 
 2174   if (!showH) viewer.setStyle({ elem: "H" }, { stick: { hidden: true }, sphere: { hidden: true } });
 2175 
 2176   if (!keepView) viewer.zoomTo();
 2177 
 2178   viewer.render();
 2179 }
 2180 
 2181 function buildPlot() {
 2182   const profile = {
 2183     x: angles, y: relEnergy, mode: "lines+markers", type: "scatter",
 2184     line: { color: "#4c72b0", width: 2 },
 2185     marker: { color: "#4c72b0", size: 6, line: { color: "#fff", width: 1 } },
 2186     hovertemplate: "%{x:.0f}&deg;<br>%{y:.2f} __UNITS__<extra></extra>",
 2187     name: "RelativeEnergy"
 2188   };
 2189 
 2190   const cursor = {
 2191     x: [angles[current]], y: [relEnergy[current]], mode: "markers", type: "scatter",
 2192     marker: { color: "#dd6b3f", size: 10, line: { color: "#fff", width: 2 }, symbol: "circle" },
 2193     hoverinfo: "skip", showlegend: false, name: "selected"
 2194   };
 2195 
 2196   const layout = {
 2197     margin: { l: 56, r: 16, t: 10, b: 48 },
 2198     paper_bgcolor: "#fff", plot_bgcolor: "#eaeaf2",
 2199     xaxis: { title: "Torsion Angle (degrees)", gridcolor: "#fff", zeroline: false },
 2200     yaxis: { title: "Relative Energy (__UNITS__)", gridcolor: "#fff", zeroline: false },
 2201     showlegend: false, hovermode: "closest"
 2202   };
 2203 
 2204   Plotly.newPlot("plot", [profile, cursor], layout, { displayModeBar: false, responsive: true });
 2205 
 2206   const plotDiv = document.getElementById("plot");
 2207 
 2208   function nearestByAngle(xVal) {
 2209     let bestIdx = 0, bestDiff = Infinity;
 2210     for (let i = 0; i < angles.length; i++) {
 2211       const a = angles[i];
 2212       if (a == null) continue;
 2213       const d = Math.abs(a - xVal);
 2214       if (d < bestDiff) { bestDiff = d; bestIdx = i; }
 2215     }
 2216     return bestIdx;
 2217   }
 2218 
 2219   plotDiv.on("plotly_click", e => { if (e.points.length) select(e.points[0].pointNumber); });
 2220   plotDiv.on("plotly_hover", e => {
 2221     if (!playing && e.points.length && e.points[0].curveNumber === 0)
 2222       select(e.points[0].pointNumber);
 2223   });
 2224 
 2225   // Fallback: clicks landing in the plot area but not on a data point
 2226   // still snap to the nearest rotamer by torsion angle.
 2227   plotDiv.addEventListener("click", evt => {
 2228     const xaxis = plotDiv._fullLayout && plotDiv._fullLayout.xaxis;
 2229     if (!xaxis || typeof xaxis.p2d !== "function") return;
 2230 
 2231     const bbox = plotDiv.getBoundingClientRect();
 2232     const xPixel = evt.clientX - bbox.left - xaxis._offset;
 2233     if (xPixel < 0 || xPixel > xaxis._length) return;
 2234 
 2235     select(nearestByAngle(xaxis.p2d(xPixel)));
 2236   });
 2237 }
 2238 
 2239 function moveCursor() {
 2240   Plotly.restyle("plot", { x: [[angles[current]]], y: [[relEnergy[current]]] }, [1]);
 2241 }
 2242 
 2243 function select(idx) {
 2244   if (idx == null || idx < 0 || idx >= FRAMES.length) return;
 2245 
 2246   current = idx;
 2247   const f = FRAMES[idx];
 2248 
 2249   document.getElementById("rAngle").textContent = f.angle != null ? f.angle.toFixed(0) : "NA";
 2250   document.getElementById("rRelEnergy").textContent   = f.relEnergy   != null ? f.relEnergy.toFixed(2)   : "NA";
 2251   document.getElementById("rAbs").textContent   = f.energy!= null ? f.energy.toFixed(2): "NA";
 2252   document.getElementById("slider").value = idx;
 2253 
 2254   moveCursor();
 2255 
 2256   renderStructure(idx, document.getElementById("optFreeze").checked);
 2257 }
 2258 
 2259 function play() {
 2260   playing = true;
 2261   document.getElementById("play").innerHTML = "&#10073;&#10073; Pause";
 2262   document.getElementById("optFreeze").checked = true; // steadier during animation
 2263 
 2264   timer = setInterval(() => select((current + 1) % FRAMES.length), 120);
 2265 }
 2266 
 2267 function pause() {
 2268   playing = false;
 2269   document.getElementById("play").innerHTML = "&#9654; Play";
 2270 
 2271   clearInterval(timer);
 2272 }
 2273 
 2274 window.addEventListener("DOMContentLoaded", () => {
 2275   const slider = document.getElementById("slider");
 2276   slider.max = FRAMES.length - 1;
 2277 
 2278   slider.addEventListener("input", e => { if (playing) pause(); select(+e.target.value); });
 2279 
 2280   document.getElementById("play").addEventListener("click", () => playing ? pause() : play());
 2281   ["optSticks", "optH"].forEach(id =>
 2282     document.getElementById(id).addEventListener("change", () => renderStructure(current, true)));
 2283 
 2284   document.getElementById("optSpin").addEventListener("change", e => {
 2285     viewer.spin(e.target.checked ? "y" : false);
 2286   });
 2287 
 2288   buildPlot();
 2289   select(0);
 2290 });
 2291 
 2292 </script>
 2293 </body>
 2294 </html>
 2295 """
 2296     return ViewerTemplate