MayaChemTools

    1 #!/bin/env python
    2 #
    3 # File: RDKitClusterMolecules.py
    4 # Author: Manish Sud <msud@san.rr.com>
    5 #
    6 # Copyright (C) 2026 Manish Sud. All rights reserved.
    7 #
    8 # The functionality available in this script is implemented using RDKit, an
    9 # open source toolkit for cheminformatics developed by Greg Landrum.
   10 #
   11 # This file is part of MayaChemTools.
   12 #
   13 # MayaChemTools is free software; you can redistribute it and/or modify it under
   14 # the terms of the GNU Lesser General Public License as published by the Free
   15 # Software Foundation; either version 3 of the License, or (at your option) any
   16 # later version.
   17 #
   18 # MayaChemTools is distributed in the hope that it will be useful, but without
   19 # any warranty; without even the implied warranty of merchantability of fitness
   20 # for a particular purpose.  See the GNU Lesser General Public License for more
   21 # details.
   22 #
   23 # You should have received a copy of the GNU Lesser General Public License
   24 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
   25 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
   26 # Boston, MA, 02111-1307, USA.
   27 #
   28 
   29 from __future__ import print_function
   30 
   31 import os
   32 import sys
   33 import time
   34 import re
   35 
   36 # RDKit imports...
   37 try:
   38     from rdkit import rdBase
   39     from rdkit import Chem
   40     from rdkit.Chem import AllChem
   41     from rdkit import DataStructs
   42     from rdkit.Chem.Fingerprints import FingerprintMols
   43     from rdkit.Chem import rdMolDescriptors
   44     from rdkit.ML.Cluster import Butina
   45     from rdkit.SimDivFilters import rdSimDivPickers
   46     from rdkit.SimDivFilters.rdSimDivPickers import HierarchicalClusterPicker
   47 except ImportError as ErrMsg:
   48     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
   49     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
   50     sys.exit(1)
   51 
   52 # MayaChemTools imports...
   53 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
   54 try:
   55     from docopt import docopt
   56     import MiscUtil
   57     import RDKitUtil
   58 except ImportError as ErrMsg:
   59     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
   60     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
   61     sys.exit(1)
   62 
   63 ScriptName = os.path.basename(sys.argv[0])
   64 Options = {}
   65 OptionsInfo = {}
   66 
   67 
   68 def main():
   69     """Start execution of the script."""
   70 
   71     MiscUtil.PrintInfo(
   72         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
   73         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
   74     )
   75 
   76     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
   77 
   78     # Retrieve command line arguments and options...
   79     RetrieveOptions()
   80 
   81     # Process and validate command line arguments and options...
   82     ProcessOptions()
   83 
   84     # Perform actions required by the script...
   85     ClusterMolecules()
   86 
   87     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
   88     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
   89 
   90 
   91 def ClusterMolecules():
   92     """Cluster molecules."""
   93 
   94     Mols = RetrieveMolecules()
   95     MolsFingerprints = GenerateFingerprints(Mols)
   96     MolsClusters = PerformClustering(Mols, MolsFingerprints)
   97 
   98     WriteMolecules(MolsClusters)
   99 
  100 
  101 def PerformClustering(Mols, MolsFingerprints):
  102     """Perform clustering."""
  103 
  104     ClusteredMols = []
  105     if re.match("^Butina$", OptionsInfo["ClusteringMethod"], re.I):
  106         return PerformButinaClustering(Mols, MolsFingerprints)
  107     else:
  108         return PerformHierarchicalClustering(Mols, MolsFingerprints)
  109 
  110     return ClusteredMols
  111 
  112 
  113 def PerformButinaClustering(Mols, MolsFingerprints):
  114     """Perform clustering using Butina methodology."""
  115 
  116     MiscUtil.PrintInfo(
  117         "\nClustering molecules using Butina methodology and %s similarity metric..." % OptionsInfo["SimilarityMetric"]
  118     )
  119 
  120     FingerprintsCount = len(MolsFingerprints)
  121     DistanceCutoff = 1 - OptionsInfo["ButinaSimilarityCutoff"]
  122     Reordering = OptionsInfo["ButinaReordering"]
  123 
  124     DistanceMatrix = GenerateLowerTriangularDistanceMatrix(MolsFingerprints)
  125 
  126     ClusteredMolIndices = Butina.ClusterData(
  127         DistanceMatrix, FingerprintsCount, DistanceCutoff, reordering=Reordering, isDistData=True
  128     )
  129 
  130     MolsClusters = []
  131     for Cluster in ClusteredMolIndices:
  132         MolsCluster = [Mols[MolIndex] for MolIndex in Cluster]
  133         MolsClusters.append(MolsCluster)
  134 
  135     return MolsClusters
  136 
  137 
  138 def PerformHierarchicalClustering(Mols, MolsFingerprints):
  139     """Perform hierarchical clustering."""
  140 
  141     try:
  142         import numpy
  143     except ImportError:
  144         MiscUtil.PrintError(
  145             "Failed to import numpy python module. This is required to cluster molecules using hierarchical clustering methodology."
  146         )
  147 
  148     if OptionsInfo["NumClusters"] > len(Mols):
  149         MiscUtil.PrintError(
  150             'The number of clusters, %d, specified using "-n, --numClusters" must be less than total number of valid molecules, %d'
  151             % (OptionsInfo["NumClusters"], len(Mols))
  152         )
  153 
  154     MiscUtil.PrintInfo(
  155         "\nCluster molecules using %s hierarchical clustering methodology and %s similarity metric..."
  156         % (OptionsInfo["SpecifiedHierarchicalClusteringMethod"], OptionsInfo["SimilarityMetric"])
  157     )
  158 
  159     NumFingerprints = len(MolsFingerprints)
  160     NumClusters = OptionsInfo["NumClusters"]
  161     DistanceMatrix = GenerateLowerTriangularDistanceMatrix(MolsFingerprints)
  162 
  163     ClusterPicker = HierarchicalClusterPicker(OptionsInfo["SpecifiedHierarchicalClusteringMethodID"])
  164     ClusteredMolIndices = ClusterPicker.Cluster(numpy.asarray(DistanceMatrix), NumFingerprints, NumClusters)
  165 
  166     MolsClusters = []
  167     for Cluster in ClusteredMolIndices:
  168         MolsCluster = [Mols[MolIndex] for MolIndex in Cluster]
  169         MolsClusters.append(MolsCluster)
  170 
  171     return MolsClusters
  172 
  173 
  174 def WriteMolecules(MolsClusters):
  175     """Write out molecules for each cluster along with cluster numbers."""
  176 
  177     ClustersCount = len(MolsClusters)
  178 
  179     SingleOutFileMode = OptionsInfo["SingleOutFileMode"]
  180     TextOutFileMode = OptionsInfo["TextOutFileMode"]
  181     TextOutFileDelim = OptionsInfo["TextOutFileDelim"]
  182 
  183     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
  184 
  185     SMILESIsomeric = OptionsInfo["OutfileParams"]["SMILESIsomeric"]
  186     SMILESKekulize = OptionsInfo["OutfileParams"]["SMILESKekulize"]
  187 
  188     # Setup outfile names and writers...
  189     SetupClustersOutFilesNames(len(MolsClusters))
  190     SingleClusterWriter, ClustersOutfilesWriters = SetupMoleculeWriters(ClustersCount)
  191 
  192     MolCount = 0
  193     SingleMolClustersCount = 0
  194 
  195     if SingleOutFileMode:
  196         Writer = SingleClusterWriter
  197 
  198     for ClusterIndex in range(0, ClustersCount):
  199         MolsCluster = MolsClusters[ClusterIndex]
  200         ClusterNum = ClusterIndex + 1
  201 
  202         if len(MolsCluster) == 1:
  203             SingleMolClustersCount += 1
  204 
  205         if not SingleOutFileMode:
  206             Writer = ClustersOutfilesWriters[ClusterIndex]
  207 
  208         for Mol in MolsCluster:
  209             MolCount += 1
  210 
  211             if TextOutFileMode:
  212                 # Write out text file including SMILES file...
  213                 SMILES = Chem.MolToSmiles(Mol, isomericSmiles=SMILESIsomeric, kekuleSmiles=SMILESKekulize)
  214                 MolName = RDKitUtil.GetMolName(Mol, MolCount)
  215                 Line = TextOutFileDelim.join([SMILES, MolName, "%d" % ClusterNum])
  216                 Writer.write("%s\n" % Line)
  217             else:
  218                 # Write out SD file...
  219                 Mol.SetProp("ClusterNumber", "%s" % ClusterNum)
  220                 if Compute2DCoords:
  221                     AllChem.Compute2DCoords(Mol)
  222                 Writer.write(Mol)
  223 
  224     if SingleClusterWriter is not None:
  225         SingleClusterWriter.close()
  226     for ClusterOutfileWriter in ClustersOutfilesWriters:
  227         ClusterOutfileWriter.close()
  228 
  229     MiscUtil.PrintInfo("\nTotal number of clusters: %d" % ClustersCount)
  230 
  231     if ClustersCount > 0:
  232         MiscUtil.PrintInfo("\nNumber of clusters containing only a single molecule: %d" % SingleMolClustersCount)
  233         MiscUtil.PrintInfo("Average number of molecules per cluster: %.1f" % (MolCount / ClustersCount))
  234 
  235         MiscUtil.PrintInfo("\nNumber of molecules in each cluster:")
  236         MiscUtil.PrintInfo("ClusterNumber,MolCount")
  237         ClusterNum = 0
  238         for MolsCluster in MolsClusters:
  239             ClusterNum += 1
  240             MiscUtil.PrintInfo("%d,%d" % (ClusterNum, len(MolsCluster)))
  241 
  242 
  243 def RetrieveMolecules():
  244     """Retrieve molecules."""
  245 
  246     Infile = OptionsInfo["Infile"]
  247 
  248     # Read molecules...
  249     MiscUtil.PrintInfo("\nReading file %s..." % Infile)
  250     OptionsInfo["InfileParams"]["AllowEmptyMols"] = False
  251     ValidMols, MolCount, ValidMolCount = RDKitUtil.ReadAndValidateMolecules(Infile, **OptionsInfo["InfileParams"])
  252 
  253     MiscUtil.PrintInfo("Total number of molecules: %d" % MolCount)
  254     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
  255     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
  256 
  257     return ValidMols
  258 
  259 
  260 def GenerateFingerprints(Mols):
  261     """Generate fingerprints."""
  262 
  263     FingerprintsName = OptionsInfo["SpecifiedFingerprints"]
  264 
  265     MolsFingerprints = []
  266     if re.match("^AtomPairs$", FingerprintsName, re.I):
  267         return GenerateAtomPairsFingerprints(Mols)
  268     elif re.match("^MACCS166Keys$", FingerprintsName, re.I):
  269         return GenerateMACCS166KeysFingerprints(Mols)
  270     elif re.match("^Morgan$", FingerprintsName, re.I):
  271         return GenerateMorganFingerprints(Mols)
  272     elif re.match("^MorganFeatures$", FingerprintsName, re.I):
  273         return GenerateMorganFeaturesFingerprints(Mols)
  274     elif re.match("^PathLength$", FingerprintsName, re.I):
  275         return GeneratePathLengthFingerprints(Mols)
  276     elif re.match("^TopologicalTorsions$", FingerprintsName, re.I):
  277         return GenerateTopologicalTorsionsFingerprints(Mols)
  278     else:
  279         MiscUtil.PrintError("Fingerprints name, %s, is not a valid name" % FingerprintsName)
  280 
  281     return MolsFingerprints
  282 
  283 
  284 def GenerateAtomPairsFingerprints(Mols):
  285     """Generate AtomPairs fingerprints."""
  286 
  287     MiscUtil.PrintInfo("\nGenerating AtomPairs %s fingerprints..." % OptionsInfo["SpecifiedFingerprintsType"])
  288 
  289     MinLength = OptionsInfo["FingerprintsParams"]["AtomPairs"]["MinLength"]
  290     MaxLength = OptionsInfo["FingerprintsParams"]["AtomPairs"]["MaxLength"]
  291     UseChirality = OptionsInfo["FingerprintsParams"]["AtomPairs"]["UseChirality"]
  292     FPSize = OptionsInfo["FingerprintsParams"]["AtomPairs"]["FPSize"]
  293     BitsPerHash = OptionsInfo["FingerprintsParams"]["AtomPairs"]["BitsPerHash"]
  294 
  295     if re.match("^BitVect$", OptionsInfo["SpecifiedFingerprintsType"], re.I):
  296         # Generate ExplicitBitVect fingerprints...
  297         MiscUtil.PrintInfo("FPSize: %s; BitsPerHash: %s" % (FPSize, BitsPerHash))
  298         MolsFingerprints = [
  299             rdMolDescriptors.GetHashedAtomPairFingerprintAsBitVect(
  300                 Mol,
  301                 minLength=MinLength,
  302                 maxLength=MaxLength,
  303                 includeChirality=UseChirality,
  304                 nBits=FPSize,
  305                 nBitsPerEntry=BitsPerHash,
  306             )
  307             for Mol in Mols
  308         ]
  309     else:
  310         # Generate IntSparseIntVect fingerprints...
  311         MolsFingerprints = [
  312             rdMolDescriptors.GetAtomPairFingerprint(
  313                 Mol, minLength=MinLength, maxLength=MaxLength, includeChirality=UseChirality
  314             )
  315             for Mol in Mols
  316         ]
  317 
  318     return MolsFingerprints
  319 
  320 
  321 def GenerateMACCS166KeysFingerprints(Mols):
  322     """Generate MACCS166Keys fingerprints."""
  323 
  324     MiscUtil.PrintInfo("\nGenerating MACCS166Keys %s fingerprints..." % OptionsInfo["SpecifiedFingerprintsType"])
  325 
  326     # Generate ExplicitBitVect fingerprints...
  327     MolsFingerprints = [rdMolDescriptors.GetMACCSKeysFingerprint(Mol) for Mol in Mols]
  328 
  329     return MolsFingerprints
  330 
  331 
  332 def GenerateMorganFingerprints(Mols):
  333     """Generate Morgan fingerprints."""
  334 
  335     MiscUtil.PrintInfo("\nGenerating Morgan %s fingerprints..." % OptionsInfo["SpecifiedFingerprintsType"])
  336 
  337     Radius = OptionsInfo["FingerprintsParams"]["Morgan"]["Radius"]
  338     UseChirality = OptionsInfo["FingerprintsParams"]["Morgan"]["UseChirality"]
  339     FPSize = OptionsInfo["FingerprintsParams"]["Morgan"]["FPSize"]
  340     UseFeatures = False
  341 
  342     if re.match("^BitVect$", OptionsInfo["SpecifiedFingerprintsType"], re.I):
  343         # Generate ExplicitBitVect fingerprints...
  344         MiscUtil.PrintInfo("FPSize: %s" % (FPSize))
  345         MolsFingerprints = [
  346             rdMolDescriptors.GetMorganFingerprintAsBitVect(
  347                 Mol, Radius, useFeatures=UseFeatures, useChirality=UseChirality, nBits=FPSize
  348             )
  349             for Mol in Mols
  350         ]
  351     else:
  352         # Generate UIntSparseIntVect fingerprints...
  353         MolsFingerprints = [
  354             rdMolDescriptors.GetMorganFingerprint(Mol, Radius, useFeatures=UseFeatures, useChirality=UseChirality)
  355             for Mol in Mols
  356         ]
  357 
  358     return MolsFingerprints
  359 
  360 
  361 def GenerateMorganFeaturesFingerprints(Mols):
  362     """Generate MorganFeatures fingerprints."""
  363 
  364     MiscUtil.PrintInfo("\nGenerating MorganFeatures %s fingerprints..." % OptionsInfo["SpecifiedFingerprintsType"])
  365 
  366     # Setup fingerprints parameters...
  367     Radius = OptionsInfo["FingerprintsParams"]["MorganFeatures"]["Radius"]
  368     UseChirality = OptionsInfo["FingerprintsParams"]["MorganFeatures"]["UseChirality"]
  369     FPSize = OptionsInfo["FingerprintsParams"]["MorganFeatures"]["FPSize"]
  370     UseFeatures = True
  371 
  372     if re.match("^BitVect$", OptionsInfo["SpecifiedFingerprintsType"], re.I):
  373         # Generate ExplicitBitVect fingerprints...
  374         MiscUtil.PrintInfo("FPSize: %s" % (FPSize))
  375         MolsFingerprints = [
  376             rdMolDescriptors.GetMorganFingerprintAsBitVect(
  377                 Mol, Radius, useFeatures=UseFeatures, useChirality=UseChirality, nBits=FPSize
  378             )
  379             for Mol in Mols
  380         ]
  381     else:
  382         # Generate UIntSparseIntVect fingerprints...
  383         MolsFingerprints = [
  384             rdMolDescriptors.GetMorganFingerprint(Mol, Radius, useFeatures=UseFeatures, useChirality=UseChirality)
  385             for Mol in Mols
  386         ]
  387 
  388     return MolsFingerprints
  389 
  390 
  391 def GeneratePathLengthFingerprints(Mols):
  392     """Generate PathLength fingerprints."""
  393 
  394     MiscUtil.PrintInfo("\nGenerating PathLength %s fingerprints..." % OptionsInfo["SpecifiedFingerprintsType"])
  395 
  396     MinPath = OptionsInfo["FingerprintsParams"]["PathLength"]["MinPath"]
  397     MaxPath = OptionsInfo["FingerprintsParams"]["PathLength"]["MaxPath"]
  398     FPSize = OptionsInfo["FingerprintsParams"]["PathLength"]["FPSize"]
  399     BitsPerHash = OptionsInfo["FingerprintsParams"]["PathLength"]["BitsPerHash"]
  400     UseHs = False
  401     TargetDensity = 0.3
  402     MinSize = 54
  403 
  404     # Generate ExplicitBitVect fingerprints...
  405     MiscUtil.PrintInfo("FPSize: %s; BitsPerHash: %s" % (FPSize, BitsPerHash))
  406     MolsFingerprints = [
  407         FingerprintMols.FingerprintMol(
  408             Mol,
  409             minPath=MinPath,
  410             maxPath=MaxPath,
  411             fpSize=FPSize,
  412             bitsPerHash=BitsPerHash,
  413             useHs=UseHs,
  414             tgtDensity=TargetDensity,
  415             minSize=MinSize,
  416         )
  417         for Mol in Mols
  418     ]
  419 
  420     return MolsFingerprints
  421 
  422 
  423 def GenerateTopologicalTorsionsFingerprints(Mols):
  424     """Generate TopologicalTorsions fingerprints."""
  425 
  426     MiscUtil.PrintInfo("\nGenerating TopologicalTorsions %s fingerprints..." % OptionsInfo["SpecifiedFingerprintsType"])
  427 
  428     UseChirality = OptionsInfo["FingerprintsParams"]["TopologicalTorsions"]["UseChirality"]
  429     FPSize = OptionsInfo["FingerprintsParams"]["TopologicalTorsions"]["FPSize"]
  430     BitsPerHash = OptionsInfo["FingerprintsParams"]["TopologicalTorsions"]["BitsPerHash"]
  431 
  432     if re.match("^BitVect$", OptionsInfo["SpecifiedFingerprintsType"], re.I):
  433         # Generate ExplicitBitVect fingerprints...
  434         MiscUtil.PrintInfo("FPSize: %s; BitsPerHash: %s" % (FPSize, BitsPerHash))
  435         MolsFingerprints = [
  436             rdMolDescriptors.GetHashedTopologicalTorsionFingerprintAsBitVect(
  437                 Mol, includeChirality=UseChirality, nBits=FPSize, nBitsPerEntry=BitsPerHash
  438             )
  439             for Mol in Mols
  440         ]
  441     else:
  442         # Generate LongSparseIntVect fingerprint...
  443         MolsFingerprints = [
  444             rdMolDescriptors.GetTopologicalTorsionFingerprint(Mol, includeChirality=UseChirality) for Mol in Mols
  445         ]
  446 
  447     return MolsFingerprints
  448 
  449 
  450 def GenerateLowerTriangularDistanceMatrix(MolsFingerprints):
  451     """Generate a lower triangular distance matrix without the diagonal."""
  452 
  453     SimilarityFunction = OptionsInfo["SimilarityFunction"]
  454 
  455     DistanceMatrix = []
  456     NumFPs = len(MolsFingerprints)
  457     for Index1 in range(0, NumFPs):
  458         for Index2 in range(0, Index1):
  459             Distance = 1 - SimilarityFunction(
  460                 MolsFingerprints[Index1],
  461                 MolsFingerprints[Index2],
  462             )
  463             DistanceMatrix.append(Distance)
  464 
  465     return DistanceMatrix
  466 
  467 
  468 def SetupMoleculeWriters(ClustersCount):
  469     """Set up molecule writers for SD and text files."""
  470 
  471     Writer = None
  472     ClustersOutfilesWriters = []
  473 
  474     TextOutFileMode = OptionsInfo["TextOutFileMode"]
  475     TextOutFileDelim = OptionsInfo["TextOutFileDelim"]
  476     TextOutFileTitleLine = OptionsInfo["TextOutFileTitleLine"]
  477 
  478     if OptionsInfo["SingleOutFileMode"]:
  479         Outfile = OptionsInfo["Outfile"]
  480         if TextOutFileMode:
  481             Writer = open(Outfile, "w")
  482         else:
  483             Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
  484         if Writer is None:
  485             MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
  486 
  487         if TextOutFileMode:
  488             if TextOutFileTitleLine:
  489                 WriteTextFileHeaderLine(Writer, TextOutFileDelim)
  490 
  491         MiscUtil.PrintInfo("Generating file %s..." % Outfile)
  492     else:
  493         for ClusterIndex in range(0, ClustersCount):
  494             Outfile = OptionsInfo["ClustersOutfiles"][ClusterIndex]
  495             if TextOutFileMode:
  496                 ClusterWriter = open(Outfile, "w")
  497             else:
  498                 ClusterWriter = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
  499             if ClusterWriter is None:
  500                 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
  501 
  502             if TextOutFileMode:
  503                 if TextOutFileTitleLine:
  504                     WriteTextFileHeaderLine(ClusterWriter, TextOutFileDelim)
  505 
  506             ClustersOutfilesWriters.append(ClusterWriter)
  507 
  508         if ClustersCount > 4:
  509             MiscUtil.PrintInfo(
  510                 "Generating %d output files with the following file name format: %s_Cluster<Num>.%s"
  511                 % (ClustersCount, OptionsInfo["OutfileBasename"], OptionsInfo["OutfileExt"])
  512             )
  513         else:
  514             Delmiter = ","
  515             OutfileNames = Delmiter.join(OptionsInfo["ClustersOutfiles"])
  516             MiscUtil.PrintInfo("Generating %d output files: %s..." % (ClustersCount, OutfileNames))
  517 
  518     return (Writer, ClustersOutfilesWriters)
  519 
  520 
  521 def WriteTextFileHeaderLine(Writer, TextOutFileDelim):
  522     """Write out a header line for text files including SMILES file."""
  523 
  524     Line = TextOutFileDelim.join(["SMILES", "Name", "ClusterNumber"])
  525     Writer.write("%s\n" % Line)
  526 
  527 
  528 def SetupClustersOutFilesNames(ClustersCount):
  529     """Set up out file names for clusters."""
  530 
  531     OptionsInfo["ClustersOutfiles"] = []
  532     if OptionsInfo["SingleOutFileMode"] or ClustersCount == 0:
  533         # Nothing to do...
  534         return
  535 
  536     OutfileBasename = OptionsInfo["OutfileBasename"]
  537     OutfileExt = OptionsInfo["OutfileExt"]
  538 
  539     ClusterOutfiles = []
  540     for ClusterIndex in range(0, ClustersCount):
  541         ClusterNum = ClusterIndex + 1
  542         ClusterOutfile = "%s_Cluster%d.%s" % (OutfileBasename, ClusterNum, OutfileExt)
  543         ClusterOutfiles.append(ClusterOutfile)
  544 
  545     OptionsInfo["ClustersOutfiles"] = ClusterOutfiles
  546 
  547 
  548 def ProcessFingerprintsParameters():
  549     """Set up and process fingerprints parameters."""
  550 
  551     SetupFingerprintsNamesAndParameters()
  552 
  553     ProcessSpecifiedFingerprintsName()
  554     ProcessSpecifiedFingerprintsType()
  555 
  556     ProcessSpecifiedFingerprintsParameters()
  557 
  558 
  559 def SetupFingerprintsNamesAndParameters():
  560     """Set up fingerprints parameters."""
  561 
  562     OptionsInfo["FingerprintsNames"] = [
  563         "AtomPairs",
  564         "MACCS166Keys",
  565         "Morgan",
  566         "MorganFeatures",
  567         "PathLength",
  568         "TopologicalTorsions",
  569     ]
  570 
  571     OptionsInfo["FingerprintsParams"] = {}
  572     OptionsInfo["FingerprintsParams"]["AtomPairs"] = {
  573         "MinLength": 1,
  574         "MaxLength": 30,
  575         "UseChirality": False,
  576         "FPSize": 2048,
  577         "BitsPerHash": 2,
  578     }
  579     OptionsInfo["FingerprintsParams"]["MACCS166Keys"] = {}
  580     OptionsInfo["FingerprintsParams"]["Morgan"] = {"Radius": 2, "UseChirality": False, "FPSize": 2048}
  581     OptionsInfo["FingerprintsParams"]["MorganFeatures"] = {"Radius": 2, "UseChirality": False, "FPSize": 2048}
  582     OptionsInfo["FingerprintsParams"]["TopologicalTorsions"] = {"UseChirality": False, "FPSize": 2048, "BitsPerHash": 4}
  583     OptionsInfo["FingerprintsParams"]["PathLength"] = {"MinPath": 1, "MaxPath": 7, "FPSize": 2048, "BitsPerHash": 2}
  584 
  585 
  586 def ProcessSpecifiedFingerprintsName():
  587     """Process specified fingerprints name."""
  588 
  589     #  Set up a canonical fingerprints name map...
  590     CanonicalFingerprintsNamesMap = {}
  591     for Name in OptionsInfo["FingerprintsNames"]:
  592         CanonicalName = Name.lower()
  593         CanonicalFingerprintsNamesMap[CanonicalName] = Name
  594 
  595     # Validate specified fingerprints name...
  596     CanonicalFingerprintsName = OptionsInfo["Fingerprints"].lower()
  597     if CanonicalFingerprintsName not in CanonicalFingerprintsNamesMap:
  598         MiscUtil.PrintError(
  599             'The fingerprints name, %s, specified using "-f, --fingerprints" option is not a valid name.'
  600             % (OptionsInfo["Fingerprints"])
  601         )
  602 
  603     OptionsInfo["SpecifiedFingerprints"] = CanonicalFingerprintsNamesMap[CanonicalFingerprintsName]
  604 
  605 
  606 def ProcessSpecifiedFingerprintsType():
  607     """Process specified fingerprints type."""
  608 
  609     FingerprintsName = OptionsInfo["SpecifiedFingerprints"]
  610     FingerprintsType = OptionsInfo["FingerprintsType"]
  611     SimilarityName = OptionsInfo["SimilarityMetric"]
  612 
  613     if re.match("^auto$", FingerprintsType, re.I):
  614         if re.match("^(MACCS166Keys|PathLength)$", FingerprintsName, re.I):
  615             SpecifiedFingerprintsType = "BitVect"
  616         else:
  617             if re.match("^(Tanimoto|Dice)$", SimilarityName, re.I):
  618                 SpecifiedFingerprintsType = "IntVect"
  619             else:
  620                 SpecifiedFingerprintsType = "BitVect"
  621     elif re.match("^IntVect$", FingerprintsType, re.I):
  622         SpecifiedFingerprintsType = "IntVect"
  623 
  624         if re.match("^(MACCS166Keys|PathLength)$", FingerprintsName, re.I):
  625             MiscUtil.PrintError(
  626                 'The fingerprints Type, %s, specified using "--fingerprintsType" is not allowed for fingerprints %s.'
  627                 % (FingerprintsType, FingerprintsName)
  628             )
  629 
  630         # RDKit similarity functions, besides Dice and Tanimoto, are not able to handle int bit vectors...
  631         if not re.match("^(Tanimoto|Dice)$", SimilarityName, re.I):
  632             MiscUtil.PrintError(
  633                 'The fingerprints Type, %s, specified using "--fingerprintsType" is not allowed for similarity metric %s.\nSupported similarity metrics: Tanimoto or Dice'
  634                 % (FingerprintsType, SimilarityName)
  635             )
  636     elif re.match("^BitVect$", FingerprintsType, re.I):
  637         SpecifiedFingerprintsType = "BitVect"
  638     else:
  639         MiscUtil.PrintError("The fingerprints Type, %s, is not supported." % (FingerprintsType))
  640 
  641     OptionsInfo["SpecifiedFingerprintsType"] = SpecifiedFingerprintsType
  642 
  643 
  644 def ProcessSpecifiedFingerprintsParameters():
  645     """Process specified fingerprints parameters."""
  646 
  647     if re.match("^auto$", OptionsInfo["ParamsFingerprints"], re.I):
  648         # Nothing to process...
  649         return
  650 
  651     SpecifiedFingerprintsName = OptionsInfo["SpecifiedFingerprints"]
  652 
  653     # Parse specified fingerprints parameters...
  654     ParamsFingerprints = re.sub(" ", "", OptionsInfo["ParamsFingerprints"])
  655     if not ParamsFingerprints:
  656         MiscUtil.PrintError(
  657             'No valid parameter name and value pairs specified using "-p, --paramsFingerprints" option corrresponding to fingerprints %s.'
  658             % (SpecifiedFingerprintsName)
  659         )
  660 
  661     ParamsFingerprintsWords = ParamsFingerprints.split(",")
  662     if len(ParamsFingerprintsWords) % 2:
  663         MiscUtil.PrintError(
  664             'The number of comma delimited paramater names and values, %d, specified using "-p, --paramsFingerprints" option must be an even number.'
  665             % (len(ParamsFingerprintsWords))
  666         )
  667 
  668     # Setup canonical parameter names for specified fingerprints...
  669     ValidParamNames = []
  670     CanonicalParamNamesMap = {}
  671     for ParamName in sorted(OptionsInfo["FingerprintsParams"][SpecifiedFingerprintsName]):
  672         ValidParamNames.append(ParamName)
  673         CanonicalParamNamesMap[ParamName.lower()] = ParamName
  674 
  675     # Validate and set paramater names and value...
  676     for Index in range(0, len(ParamsFingerprintsWords), 2):
  677         Name = ParamsFingerprintsWords[Index]
  678         Value = ParamsFingerprintsWords[Index + 1]
  679 
  680         CanonicalName = Name.lower()
  681         if CanonicalName not in CanonicalParamNamesMap:
  682             MiscUtil.PrintError(
  683                 'The parameter name, %s, specified using "-p, --paramsFingerprints" option for fingerprints, %s, is not a valid name. Supported parameter names: %s'
  684                 % (Name, SpecifiedFingerprintsName, " ".join(ValidParamNames))
  685             )
  686 
  687         ParamName = CanonicalParamNamesMap[CanonicalName]
  688         if re.match("^UseChirality$", ParamName, re.I):
  689             if not re.match("^(Yes|No|True|False)$", Value, re.I):
  690                 MiscUtil.PrintError(
  691                     'The parameter value, %s, specified using "-p, --paramsFingerprints" option for fingerprints, %s, is not a valid value. Supported values: Yes No True False'
  692                     % (Value, SpecifiedFingerprintsName)
  693                 )
  694             ParamValue = False
  695             if re.match("^(Yes|True)$", Value, re.I):
  696                 ParamValue = True
  697         else:
  698             ParamValue = int(Value)
  699             if ParamValue <= 0:
  700                 MiscUtil.PrintError(
  701                     'The parameter value, %s, specified using "-p, --paramsFingerprints" option for fingerprints, %s, is not a valid value. Supported values: > 0'
  702                     % (Value, SpecifiedFingerprintsName)
  703                 )
  704 
  705         # Set value...
  706         OptionsInfo["FingerprintsParams"][SpecifiedFingerprintsName][ParamName] = ParamValue
  707 
  708 
  709 def ProcessSimilarityMetricParameter():
  710     """Process specified similarity metric value."""
  711 
  712     SimilarityInfoMap = {}
  713     CanonicalNameMap = {}
  714 
  715     for SimilarityFunctionInfo in DataStructs.similarityFunctions:
  716         Name = SimilarityFunctionInfo[0]
  717         Function = SimilarityFunctionInfo[1]
  718 
  719         SimilarityInfoMap[Name] = Function
  720         CanonicalName = Name.lower()
  721         CanonicalNameMap[CanonicalName] = Name
  722 
  723     SpecifiedCanonicalName = OptionsInfo["SimilarityMetric"].lower()
  724     SimilarityFunction = None
  725     if SpecifiedCanonicalName in CanonicalNameMap:
  726         SimilarityName = CanonicalNameMap[SpecifiedCanonicalName]
  727         SimilarityFunction = SimilarityInfoMap[SimilarityName]
  728     else:
  729         MiscUtil.PrintError("Similarity metric name, %s, is not a valid name. " % OptionsInfo["SimilarityMetric"])
  730 
  731     OptionsInfo["SimilarityMetric"] = SimilarityName
  732     OptionsInfo["SimilarityFunction"] = SimilarityFunction
  733 
  734 
  735 def ProcessClusteringMethodParameter():
  736     """Process specified clustering method parameter."""
  737 
  738     OptionsInfo["SpecifiedHierarchicalClusteringMethod"] = ""
  739     OptionsInfo["SpecifiedHierarchicalClusteringMethodID"] = ""
  740 
  741     if re.match("^Butina$", OptionsInfo["ClusteringMethod"], re.I):
  742         # Nothing to process...
  743         return
  744 
  745     # Setup a canonical cluster method name map..
  746     ClusteringMethodInfoMap = {}
  747     CanonicalClusteringMethodNameMap = {}
  748     for Name in sorted(rdSimDivPickers.ClusterMethod.names):
  749         NameID = rdSimDivPickers.ClusterMethod.names[Name]
  750         ClusteringMethodInfoMap[Name] = NameID
  751 
  752         CanonicalName = Name.lower()
  753         CanonicalClusteringMethodNameMap[CanonicalName] = Name
  754 
  755     CanonicalName = OptionsInfo["ClusteringMethod"].lower()
  756     if CanonicalName not in CanonicalClusteringMethodNameMap:
  757         MiscUtil.PrintError(
  758             'The clustering method, %s, specified using "-c, --clusteringMethod" option is not a valid name.'
  759             % (OptionsInfo["ClusteringMethod"])
  760         )
  761 
  762     SpecifiedHierarchicalClusteringMethodName = CanonicalClusteringMethodNameMap[CanonicalName]
  763     OptionsInfo["SpecifiedHierarchicalClusteringMethod"] = SpecifiedHierarchicalClusteringMethodName
  764     OptionsInfo["SpecifiedHierarchicalClusteringMethodID"] = ClusteringMethodInfoMap[
  765         SpecifiedHierarchicalClusteringMethodName
  766     ]
  767 
  768 
  769 def ProcessOptions():
  770     """Process and validate command line arguments and options."""
  771 
  772     MiscUtil.PrintInfo("Processing options...")
  773 
  774     # Validate options...
  775     ValidateOptions()
  776 
  777     OptionsInfo["ButinaSimilarityCutoff"] = float(Options["--butinaSimilarityCutoff"])
  778     OptionsInfo["ButinaReordering"] = False
  779     if re.match("^Yes$", Options["--butinaReordering"], re.I):
  780         OptionsInfo["ButinaReordering"] = True
  781 
  782     OptionsInfo["Fingerprints"] = Options["--fingerprints"]
  783     OptionsInfo["FingerprintsType"] = Options["--fingerprintsType"]
  784 
  785     OptionsInfo["ClusteringMethod"] = Options["--clusteringMethod"]
  786     ProcessClusteringMethodParameter()
  787 
  788     OptionsInfo["NumClusters"] = int(Options["--numClusters"])
  789 
  790     OptionsInfo["Infile"] = Options["--infile"]
  791     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
  792         "--infileParams", Options["--infileParams"], Options["--infile"]
  793     )
  794 
  795     OptionsInfo["Outfile"] = Options["--outfile"]
  796     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
  797         "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"]
  798     )
  799 
  800     OptionsInfo["Overwrite"] = Options["--overwrite"]
  801 
  802     OptionsInfo["OutFileMode"] = Options["--outfileMode"]
  803     SingleOutFileMode = True
  804     if not re.match("^SingleFile$", Options["--outfileMode"], re.I):
  805         SingleOutFileMode = False
  806     OptionsInfo["SingleOutFileMode"] = SingleOutFileMode
  807 
  808     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
  809     OptionsInfo["OutfileBasename"] = FileName
  810     OptionsInfo["OutfileExt"] = FileExt
  811 
  812     TextOutFileMode = False
  813     TextOutFileDelim = ""
  814     TextOutFileTitleLine = True
  815 
  816     if MiscUtil.CheckFileExt(Options["--outfile"], "csv"):
  817         TextOutFileMode = True
  818         TextOutFileDelim = ","
  819     elif MiscUtil.CheckFileExt(Options["--outfile"], "tsv txt"):
  820         TextOutFileMode = True
  821         TextOutFileDelim = "\t"
  822     elif MiscUtil.CheckFileExt(Options["--outfile"], "smi"):
  823         TextOutFileMode = True
  824         TextOutFileDelim = OptionsInfo["OutfileParams"]["SMILESDelimiter"]
  825         TextOutFileTitleLine = OptionsInfo["OutfileParams"]["SMILESTitleLine"]
  826 
  827     OptionsInfo["TextOutFileMode"] = TextOutFileMode
  828     OptionsInfo["TextOutFileDelim"] = TextOutFileDelim
  829     OptionsInfo["TextOutFileTitleLine"] = TextOutFileTitleLine
  830 
  831     OptionsInfo["SimilarityMetric"] = Options["--similarityMetric"]
  832     ProcessSimilarityMetricParameter()
  833 
  834     OptionsInfo["ParamsFingerprints"] = Options["--paramsFingerprints"]
  835     ProcessFingerprintsParameters()
  836 
  837 
  838 def RetrieveOptions():
  839     """Retrieve command line arguments and options."""
  840 
  841     # Get options...
  842     global Options
  843     Options = docopt(_docoptUsage_)
  844 
  845     # Set current working directory to the specified directory...
  846     WorkingDir = Options["--workingdir"]
  847     if WorkingDir:
  848         os.chdir(WorkingDir)
  849 
  850     # Handle examples option...
  851     if "--examples" in Options and Options["--examples"]:
  852         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
  853         sys.exit(0)
  854 
  855 
  856 def ValidateOptions():
  857     """Validate option values."""
  858 
  859     MiscUtil.ValidateOptionFloatValue(
  860         "-b, --butinaSimilarityCutoff", Options["--butinaSimilarityCutoff"], {">": 0.0, "<=": 1.0}
  861     )
  862     MiscUtil.ValidateOptionTextValue("--butinaReordering", Options["--butinaReordering"], "yes no")
  863 
  864     MiscUtil.ValidateOptionTextValue(
  865         "-c, --clusteringMethod", Options["--clusteringMethod"], "Butina Centroid CLink Gower McQuitty SLink UPGMA Ward"
  866     )
  867     MiscUtil.ValidateOptionTextValue(
  868         "-f, --fingerprints",
  869         Options["--fingerprints"],
  870         "AtomPairs MACCS166Keys Morgan MorganFeatures PathLength TopologicalTorsions",
  871     )
  872     MiscUtil.ValidateOptionTextValue("--fingerprintsType", Options["--fingerprintsType"], "IntVect BitVect  auto")
  873 
  874     MiscUtil.ValidateOptionIntegerValue("-n, --numClusters", Options["--numClusters"], {">": 0})
  875 
  876     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
  877     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi csv tsv txt")
  878 
  879     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi csv tsv txt")
  880     MiscUtil.ValidateOptionsOutputFileOverwrite(
  881         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
  882     )
  883     MiscUtil.ValidateOptionsDistinctFileNames(
  884         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
  885     )
  886 
  887     MiscUtil.ValidateOptionTextValue("--outfileMode", Options["--outfileMode"], "SingleFile MultipleFiles")
  888 
  889     MiscUtil.ValidateOptionTextValue(
  890         "-s, --similarityMetric",
  891         Options["--similarityMetric"],
  892         "BraunBlanquet Cosine Dice Kulczynski RogotGoldberg Russel Sokal Tanimoto",
  893     )
  894 
  895 
  896 # Setup a usage string for docopt...
  897 _docoptUsage_ = """
  898 RDKitClusterMolecules.py - Cluster molecules using 2D fingerprints
  899 
  900 Usage:
  901     RDKitClusterMolecules.py [--butinaSimilarityCutoff <number>]  [--butinaReordering <yes or no>]
  902                              [--clusteringMethod <Butina, Centroid, CLink...>] [--fingerprints <MACCS166Keys, Morgan, PathLength...> ]
  903                              [--fingerprintsType <IntVect, BitVect, or Auto>] [--infileParams <Name,Value,...>]
  904                              [--numClusters <number>] [--outfileMode <SingleFile or MultipleFiles>]
  905                              [ --outfileParams <Name,Value,...> ] [--overwrite] [--paramsFingerprints <Name,Value,...>]
  906                              [--similarityMetric <Dice, Tanimoto...>] [-w <dir>] -i <infile> -o <outfile> 
  907     RDKitClusterMolecules.py -h | --help | -e | --examples
  908 
  909 Description:
  910     Cluster molecules based on a variety of 2D fingerprints using Butina [ Ref 136 ] or any
  911     other available hierarchical clustering methodology and write them to output file(s).
  912 
  913     The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
  914     .txt, .csv, .tsv)
  915 
  916     The supported output file formats are: SD (.sdf, .sd), SMILES (.smi), CSV/TSV
  917     (.csv, .tsv, .txt)
  918 
  919 Options:
  920     -b, --butinaSimilarityCutoff <number>  [default: 0.55]
  921         Similarity cutoff to use during Butina clustering. The molecule pairs with
  922         similarity value greater than specified value or distance less than '1 - specified
  923         value' are considered neighbors. This value is only used during 'Butina' value
  924         of '-c, --clusteringMethod' option and determines the number of clusters
  925         during the clustering of molecules. It is ignored for all other clustering methods.
  926     --butinaReordering <yes or no>  [default: no]
  927         Update number of neighbors for unassigned molecules after creating a new
  928         cluster in order to insure that the molecule with the largest number of
  929         unassigned neighbors is selected as the next cluster center.
  930     -c, --clusteringMethod <Butina, Centroid, CLink...>  [default: Butina]
  931         Clustering method to use for clustering molecules. Supported values:
  932         Butina, Centroid, CLink, Gower, McQuitty, SLink, UPGMA, Ward.
  933         Butina is an unsupervised database clustering method to automatically
  934         cluster small and large data sets. All other clustering methods correspond
  935         to hierarchical clustering and require a priori specification of number of
  936         clusters to be generated.
  937     -f, --fingerprints <MACCS166Keys, Morgan, PathLength...>  [default: Morgan]
  938         Fingerprints to use for calculating similarity/distance between molecules.
  939         Supported values: AtomPairs, MACCS166Keys, Morgan, MorganFeatures, PathLength,
  940         TopologicalTorsions. The PathLength fingerprints are Daylight like fingerprints.
  941         The Morgan and MorganFeature fingerprints are circular fingerprints, corresponding
  942         Scitegic's Extended Connectivity Fingerprints (ECFP) and Features Connectivity
  943         Fingerprints (FCFP). The values of default parameters for generating fingerprints
  944         can be modified using '-p, --paramsFingerprints' option.
  945     --fingerprintsType <IntVect, BitVect, or auto>  [default: auto]
  946         Fingerprints type to generate for calculating similarity. Supported values:
  947         IntVect, BitVect, Auto.
  948         
  949         The following default fingerprints type are automatically generated for
  950         available fingerprints, based on the value of similarty metric:
  951         
  952         AtomPairs              Tanimoto|Dice: IntVect     All Others: BitVect
  953         MACCS166Keys           All: BitVect
  954         Morgan                 Tanimoto|Dice: IntVect     All Others: BitVect
  955         MorganFeatures         Tanimoto|Dice: IntVect     All Others: BitVect
  956         PathLength             All: BitVect
  957         TopologicalTorsions    Tanimoto|Dice: IntVect     All Others: BitVect
  958  
  959         The Dice and Tanimoto similarity functions available in RDKit are able to
  960         handle fingerprints corresponding to both IntVect and BitVect. All other
  961         similarity functions, however, expect BitVect fingerprints to calculate
  962         pairwise similarity. Consequently, BitVect fingerprints, instead of
  963         default IntVect fingerprints, are generated for AtomPairs, Morgan,
  964         MorganFeatures, and TopologicalTorsions during the calculation
  965         of similarity using all other similarity functions.
  966         
  967         The IntVect fingerprints type is not available for MACCS166Keys and
  968         Pathlength fingerprints. In addition, IntVect fingerprints type is only
  969         valid for Tanimoto or Dice value of ' -s, --similarityMetric' option. The
  970         BitVect fingerprints type is valid for all values of '' -s, --similarityMetric'
  971         option.
  972     -e, --examples
  973         Print examples.
  974     -h, --help
  975         Print this help message.
  976     -i, --infile <infile>
  977         Input file name.
  978     --infileParams <Name,Value,...>  [default: auto]
  979         A comma delimited list of parameter name and value pairs for reading 
  980         molecules from files. The supported parameter names for different file
  981         formats, along with their default values, are shown below:
  982             
  983             SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
  984             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
  985                 smilesTitleLine,auto,sanitize,yes
  986             
  987         Possible values for smilesDelimiter: space, comma or tab.
  988     -n, --numClusters <number>  [default: 10]
  989         Number of clusters to generate during hierarchical clustering. This option is
  990         ignored for 'Butina' value of '-c, --clusteringMethod' option.
  991     -o, --outfile <outfile>
  992         Output file name.
  993     --outfileMode <SingleFile or MultipleFiles>  [default: SingleFile]
  994         Write out a single file containing molecule clusters or generate an individual file
  995         for each cluster. Possible values: SingleFile or MultipleFiles. The molecules are
  996         grouped for each cluster before they are written to output file(s) along with
  997         appropriate cluster numbers. The cluster number is also appended to output
  998         file names during generation of multiple output files.
  999     --outfileParams <Name,Value,...>  [default: auto]
 1000         A comma delimited list of parameter name and value pairs for writing
 1001         molecules to files. The supported parameter names for different file
 1002         formats, along with their default values, are shown below:
 1003             
 1004             SD: compute2DCoords,auto,kekulize,yes,forceV3000,no
 1005             SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes,
 1006                 smilesTitleLine,yes
 1007             
 1008         Default value for compute2DCoords: yes for SMILES input file; no for all other
 1009         file types. The kekulize and smilesIsomeric parameters are also used during
 1010         generation of SMILES strings for CSV/TSV files.
 1011     --overwrite
 1012         Overwrite existing files.
 1013     -p, --paramsFingerprints <Name,Value,...>  [default: auto]
 1014         Parameter values to use for generating fingerprints. The default values
 1015         are dependent on the value of '-f, --fingerprints' option. In general, it is a
 1016         comma delimited list of parameter name and value pairs for the name of
 1017         fingerprints specified using '-f, --fingerprints' option. The supported
 1018         parameter names along with their default values for valid fingerprints
 1019         names are shown below:
 1020             
 1021             AtomPairs: minLength,1 ,maxLength,30, useChirality,No,
 1022                 fpSize, 2048, bitsPerHash,4
 1023             Morgan: radius,2, useChirality,No, fpSize, 2048
 1024             MorganFeatures:   radius,2, useChirality,No, fpSize, 2048
 1025             PathLength: minPath,1, maxPath,7, fpSize, 2048, bitsPerHash,2
 1026             TopologicalTorsions: useChirality,No, fpSize, 2048, bitsPerHash,4
 1027             
 1028         The fpSize and bitsPerHash are only used for BitVect fingerprints type
 1029         specified using '--fingerprintsType' option.
 1030     -s, --similarityMetric <Dice, Tanimoto...>  [default: Tanimoto]
 1031         Similarity metric to use for calculating similarity/distance between molecules.
 1032         Possible values: BraunBlanquet, Cosine, Dice, Kulczynski, RogotGoldberg,
 1033         Russel, Sokal, Tanimoto.
 1034     -w, --workingdir <dir>
 1035         Location of working directory which defaults to the current directory.
 1036 
 1037 Examples:
 1038     To cluster molecules using Butina methodology at a similarity cutoff of 0.55
 1039     with automatic determination of number of clusters, Tanimoto similarity
 1040     metric corresponding to Morgan fingerprints with radius of 2, and write out
 1041     a single SMILES file containing clustered molecules along with cluster number
 1042     for each molecule, type:
 1043 
 1044         % RDKitClusterMolecules.py  -i Sample.smi -o SampleOut.smi
 1045 
 1046     To cluster molecules using Butina methodology at a similarity cutoff of 0.55
 1047     with automatic determination of number of clusters, Tanimoto similarity
 1048     metric corresponding to Morgan fingerprints with radius of 2 and type
 1049     BitVect, fingerprint BitVect size of 4096, and write out a single SMILES file
 1050     containing clustered molecules along with cluster number for each molecule,
 1051     type:
 1052 
 1053         % RDKitClusterMolecules.py  -f Morgan  --fingerprintsType  BitVect
 1054           -p "fpSize,4096" -s Tanimoto -i Sample.smi -o SampleOut.smi
 1055 
 1056     To cluster molecules using Butina methodology at similarity cutoff of 0.45
 1057     with automatic determination of number of clusters, Dice similarity metric
 1058     corresponding to Morgan fingerprints with radius of 2, and write out multiple
 1059     SD files containing clustered molecules for each cluster, type:
 1060 
 1061         % RDKitClusterMolecules.py  -b 0.45 -s Dice --outfileMode MultipleFiles
 1062           -i Sample.smi -o SampleOut.sdf
 1063 
 1064     To cluster molecules using Ward hierarchical methodology to generate 15
 1065     clusters, Dice similarity metric corresponding to Pathlength fingerprints with 
 1066     path length between 1 and 7,  and write out a single TSV file for clustered
 1067     molecules along with cluster numner for each molecule, type:
 1068 
 1069         % RDKitClusterMolecules.py  -c Ward -f PathLength -n 15
 1070           -p 'minPath,1, maxPath,7' -i Sample.sdf -o SampleOut.tsv
 1071 
 1072     To cluster molecules using Centroid hierarchical methodology to generate 5
 1073     clusters, Dice similarity metric corresponding to MACCS166Keys fingerprints
 1074     for molecules in a SMILES CSV file, SMILES strings in column 1, name in
 1075     column 2, and write out a single SD file for clustered molecules along with
 1076     cluster numner for each molecule, type:
 1077 
 1078         % RDKitClusterMolecules.py  -c Centroid -f MACCS166Keys --infileParams
 1079           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
 1080           smilesNameColumn,2" --outfileParams "compute2DCoords,yes"
 1081           -i SampleSMILES.csv -o SampleOut.sdf
 1082 
 1083 Author:
 1084     Manish Sud(msud@san.rr.com)
 1085 
 1086 See also:
 1087     RDKitConvertFileFormat.py, RDKitPickDiverseMolecules.py, RDKitSearchFunctionalGroups.py,
 1088     RDKitSearchSMARTS.py
 1089 
 1090 Copyright:
 1091     Copyright (C) 2026 Manish Sud. All rights reserved.
 1092 
 1093     The functionality available in this script is implemented using RDKit, an
 1094     open source toolkit for cheminformatics developed by Greg Landrum.
 1095 
 1096     This file is part of MayaChemTools.
 1097 
 1098     MayaChemTools is free software; you can redistribute it and/or modify it under
 1099     the terms of the GNU Lesser General Public License as published by the Free
 1100     Software Foundation; either version 3 of the License, or (at your option) any
 1101     later version.
 1102 
 1103 """
 1104 
 1105 if __name__ == "__main__":
 1106     main()