MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: RDKitPickDiverseMolecules.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.Chem import AllChem
  40     from rdkit import DataStructs
  41     from rdkit.Chem.Fingerprints import FingerprintMols
  42     from rdkit.Chem import rdMolDescriptors
  43     from rdkit.SimDivFilters import rdSimDivPickers
  44     from rdkit.SimDivFilters.rdSimDivPickers import MaxMinPicker
  45     from rdkit.SimDivFilters.rdSimDivPickers import HierarchicalClusterPicker
  46 except ImportError as ErrMsg:
  47     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
  48     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
  49     sys.exit(1)
  50 
  51 # MayaChemTools imports...
  52 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  53 try:
  54     from docopt import docopt
  55     import MiscUtil
  56     import RDKitUtil
  57 except ImportError as ErrMsg:
  58     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  59     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  60     sys.exit(1)
  61 
  62 ScriptName = os.path.basename(sys.argv[0])
  63 Options = {}
  64 OptionsInfo = {}
  65 
  66 
  67 def main():
  68     """Start execution of the script."""
  69 
  70     MiscUtil.PrintInfo(
  71         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
  72         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
  73     )
  74 
  75     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  76 
  77     # Retrieve command line arguments and options...
  78     RetrieveOptions()
  79 
  80     # Process and validate command line arguments and options...
  81     ProcessOptions()
  82 
  83     # Perform actions required by the script...
  84     PickDiverseMolecules()
  85 
  86     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  87     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  88 
  89 
  90 def PickDiverseMolecules():
  91     """Pick diverse molecules."""
  92 
  93     Mols = RetrieveMolecules()
  94     MolsFingerprints = GenerateFingerprints(Mols)
  95     DiverseMols = SelectMolecules(Mols, MolsFingerprints)
  96 
  97     WriteMolecules(DiverseMols)
  98 
  99 
 100 def SelectMolecules(Mols, MolsFingerprints):
 101     """Select diverse molecules."""
 102 
 103     if OptionsInfo["NumMols"] > len(Mols):
 104         MiscUtil.PrintError(
 105             'The number of diverse molecules to pick, %d, specified using "-n, --numMols" must be less than total number of valid molecules, %d'
 106             % (OptionsInfo["NumMols"], len(Mols))
 107         )
 108 
 109     DiverseMols = []
 110     if re.match("^MaxMin$", OptionsInfo["Mode"], re.I):
 111         return SelectMoleculesUsingMaxMin(Mols, MolsFingerprints)
 112     elif re.match("^HierarchicalClustering$", OptionsInfo["Mode"], re.I):
 113         return SelectMoleculesUsingHierarchicalClustering(Mols, MolsFingerprints)
 114     else:
 115         MiscUtil.PrintError("The mode vaue, %s, is not a valid mode." % OptionsInfo["Mode"])
 116 
 117     return DiverseMols
 118 
 119 
 120 def SelectMoleculesUsingMaxMin(Mols, MolsFingerprints):
 121     """Select diverse molecules using MaxMin methodology."""
 122 
 123     MiscUtil.PrintInfo(
 124         "\nSelecting diverse molecules using MaxMin methodology and %s similarity metric..."
 125         % OptionsInfo["SimilarityMetric"]
 126     )
 127 
 128     DiverseMols = []
 129 
 130     PoolSize = len(MolsFingerprints)
 131     PickSize = OptionsInfo["NumMols"]
 132     SimilarityFunction = OptionsInfo["SimilarityFunction"]
 133 
 134     Picker = MaxMinPicker()
 135     PairwiseDistance = lambda i, j: 1 - SimilarityFunction(MolsFingerprints[i], MolsFingerprints[j])
 136 
 137     MolIndices = Picker.LazyPick(PairwiseDistance, PoolSize, PickSize)
 138 
 139     for Index in list(MolIndices):
 140         DiverseMols.append(Mols[Index])
 141 
 142     return DiverseMols
 143 
 144 
 145 def SelectMoleculesUsingHierarchicalClustering(Mols, MolsFingerprints):
 146     """Select diverse molecules using hierarchical clustering  methodology."""
 147 
 148     try:
 149         import numpy
 150     except ImportError:
 151         MiscUtil.PrintError(
 152             "Failed to import numpy python module. This is required for picking diverse molecules using hierarchical for clustering."
 153         )
 154 
 155     MiscUtil.PrintInfo(
 156         "\nSelecting diverse molecules using %s hierarchical clustering methodology..."
 157         % OptionsInfo["SpecifiedClusteringMethod"]
 158     )
 159 
 160     DiverseMols = []
 161 
 162     PoolSize = len(MolsFingerprints)
 163     PickSize = OptionsInfo["NumMols"]
 164     DistanceMatrix = GenerateLowerTriangularDistanceMatrix(MolsFingerprints)
 165 
 166     ClusterPicker = HierarchicalClusterPicker(OptionsInfo["SpecifiedClusteringMethodID"])
 167     MolIndices = ClusterPicker.Pick(numpy.asarray(DistanceMatrix), PoolSize, PickSize)
 168 
 169     for Index in MolIndices:
 170         DiverseMols.append(Mols[Index])
 171 
 172     return DiverseMols
 173 
 174 
 175 def RetrieveMolecules():
 176     """Retrieve molecules."""
 177 
 178     Infile = OptionsInfo["Infile"]
 179 
 180     # Read molecules...
 181     MiscUtil.PrintInfo("\nReading file %s..." % Infile)
 182 
 183     OptionsInfo["InfileParams"]["AllowEmptyMols"] = False
 184     ValidMols, MolCount, ValidMolCount = RDKitUtil.ReadAndValidateMolecules(Infile, **OptionsInfo["InfileParams"])
 185 
 186     MiscUtil.PrintInfo("Total number of molecules: %d" % MolCount)
 187     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
 188     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
 189 
 190     return ValidMols
 191 
 192 
 193 def GenerateFingerprints(Mols):
 194     """Generate fingerprints."""
 195 
 196     FingerprintsName = OptionsInfo["SpecifiedFingerprints"]
 197 
 198     MolsFingerprints = []
 199     if re.match("^AtomPairs$", FingerprintsName, re.I):
 200         return GenerateAtomPairsFingerprints(Mols)
 201     elif re.match("^MACCS166Keys$", FingerprintsName, re.I):
 202         return GenerateMACCS166KeysFingerprints(Mols)
 203     elif re.match("^Morgan$", FingerprintsName, re.I):
 204         return GenerateMorganFingerprints(Mols)
 205     elif re.match("^MorganFeatures$", FingerprintsName, re.I):
 206         return GenerateMorganFeaturesFingerprints(Mols)
 207     elif re.match("^PathLength$", FingerprintsName, re.I):
 208         return GeneratePathLengthFingerprints(Mols)
 209     elif re.match("^TopologicalTorsions$", FingerprintsName, re.I):
 210         return GenerateTopologicalTorsionsFingerprints(Mols)
 211     else:
 212         MiscUtil.PrintError("Fingerprints name, %s, is not a valid name" % FingerprintsName)
 213 
 214     return MolsFingerprints
 215 
 216 
 217 def GenerateAtomPairsFingerprints(Mols):
 218     """Generate AtomPairs fingerprints."""
 219 
 220     MiscUtil.PrintInfo("\nGenerating AtomPairs fingerprints...")
 221 
 222     MinLength = OptionsInfo["FingerprintsParams"]["AtomPairs"]["MinLength"]
 223     MaxLength = OptionsInfo["FingerprintsParams"]["AtomPairs"]["MaxLength"]
 224     UseChirality = OptionsInfo["FingerprintsParams"]["AtomPairs"]["UseChirality"]
 225 
 226     if OptionsInfo["GenerateBitVectFingerints"]:
 227         # Generate ExplicitBitVect fingerprints...
 228         FPSize = 2048
 229         BitsPerHash = 4
 230         MolsFingerprints = [
 231             rdMolDescriptors.GetHashedAtomPairFingerprintAsBitVect(
 232                 Mol,
 233                 minLength=MinLength,
 234                 maxLength=MaxLength,
 235                 includeChirality=UseChirality,
 236                 nBits=FPSize,
 237                 nBitsPerEntry=BitsPerHash,
 238             )
 239             for Mol in Mols
 240         ]
 241     else:
 242         # Generate IntSparseIntVect fingerprints...
 243         MolsFingerprints = [
 244             rdMolDescriptors.GetAtomPairFingerprint(
 245                 Mol, minLength=MinLength, maxLength=MaxLength, includeChirality=UseChirality
 246             )
 247             for Mol in Mols
 248         ]
 249 
 250     return MolsFingerprints
 251 
 252 
 253 def GenerateMACCS166KeysFingerprints(Mols):
 254     """Generate MACCS166Keys fingerprints."""
 255 
 256     MiscUtil.PrintInfo("\nGenerating MACCS166Keys fingerprints...")
 257 
 258     # Generate ExplicitBitVect fingerprints...
 259     MolsFingerprints = [rdMolDescriptors.GetMACCSKeysFingerprint(Mol) for Mol in Mols]
 260 
 261     return MolsFingerprints
 262 
 263 
 264 def GenerateMorganFingerprints(Mols):
 265     """Generate Morgan fingerprints."""
 266 
 267     MiscUtil.PrintInfo("\nGenerating  Morgan fingerprints...")
 268 
 269     Radius = OptionsInfo["FingerprintsParams"]["Morgan"]["Radius"]
 270     UseChirality = OptionsInfo["FingerprintsParams"]["Morgan"]["UseChirality"]
 271     UseFeatures = False
 272 
 273     if OptionsInfo["GenerateBitVectFingerints"]:
 274         # Generate ExplicitBitVect fingerprints...
 275         FPSize = 2048
 276         MolsFingerprints = [
 277             rdMolDescriptors.GetMorganFingerprintAsBitVect(
 278                 Mol, Radius, useFeatures=UseFeatures, useChirality=UseChirality, nBits=FPSize
 279             )
 280             for Mol in Mols
 281         ]
 282     else:
 283         # Generate UIntSparseIntVect fingerprints...
 284         MolsFingerprints = [
 285             rdMolDescriptors.GetMorganFingerprint(Mol, Radius, useFeatures=UseFeatures, useChirality=UseChirality)
 286             for Mol in Mols
 287         ]
 288 
 289     return MolsFingerprints
 290 
 291 
 292 def GenerateMorganFeaturesFingerprints(Mols):
 293     """Generate MorganFeatures fingerprints."""
 294 
 295     MiscUtil.PrintInfo("\nGenerating  MorganFeatures fingerprints...")
 296 
 297     # Setup fingerprints parameters...
 298     Radius = OptionsInfo["FingerprintsParams"]["MorganFeatures"]["Radius"]
 299     UseChirality = OptionsInfo["FingerprintsParams"]["MorganFeatures"]["UseChirality"]
 300     UseFeatures = True
 301 
 302     if OptionsInfo["GenerateBitVectFingerints"]:
 303         # Generate ExplicitBitVect fingerprints...
 304         FPSize = 2048
 305         MolsFingerprints = [
 306             rdMolDescriptors.GetMorganFingerprintAsBitVect(
 307                 Mol, Radius, useFeatures=UseFeatures, useChirality=UseChirality, nBits=FPSize
 308             )
 309             for Mol in Mols
 310         ]
 311     else:
 312         # Generate UIntSparseIntVect fingerprints...
 313         MolsFingerprints = [
 314             rdMolDescriptors.GetMorganFingerprint(Mol, Radius, useFeatures=UseFeatures, useChirality=UseChirality)
 315             for Mol in Mols
 316         ]
 317 
 318     return MolsFingerprints
 319 
 320 
 321 def GeneratePathLengthFingerprints(Mols):
 322     """Generate PathLength fingerprints."""
 323 
 324     MiscUtil.PrintInfo("\nGenerating PathLength fingerprints ...")
 325 
 326     MinPath = OptionsInfo["FingerprintsParams"]["PathLength"]["MinPath"]
 327     MaxPath = OptionsInfo["FingerprintsParams"]["PathLength"]["MaxPath"]
 328     FPSize = OptionsInfo["FingerprintsParams"]["PathLength"]["FPSize"]
 329     BitsPerHash = OptionsInfo["FingerprintsParams"]["PathLength"]["BitsPerHash"]
 330     UseHs = False
 331     TargetDensity = 0.3
 332     MinSize = 54
 333 
 334     # Generate ExplicitBitVect fingerprints...
 335     MolsFingerprints = [
 336         FingerprintMols.FingerprintMol(
 337             Mol,
 338             minPath=MinPath,
 339             maxPath=MaxPath,
 340             fpSize=FPSize,
 341             bitsPerHash=BitsPerHash,
 342             useHs=UseHs,
 343             tgtDensity=TargetDensity,
 344             minSize=MinSize,
 345         )
 346         for Mol in Mols
 347     ]
 348 
 349     return MolsFingerprints
 350 
 351 
 352 def GenerateTopologicalTorsionsFingerprints(Mols):
 353     """Generate TopologicalTorsions fingerprints."""
 354 
 355     MiscUtil.PrintInfo("\nGenerating TopologicalTorsions fingerprints...")
 356 
 357     UseChirality = OptionsInfo["FingerprintsParams"]["TopologicalTorsions"]["UseChirality"]
 358 
 359     if OptionsInfo["GenerateBitVectFingerints"]:
 360         FPSize = 2048
 361         BitsPerHash = 4
 362         MolsFingerprints = [
 363             rdMolDescriptors.GetHashedTopologicalTorsionFingerprintAsBitVect(
 364                 Mol, includeChirality=UseChirality, nBits=FPSize, nBitsPerEntry=BitsPerHash
 365             )
 366             for Mol in Mols
 367         ]
 368     else:
 369         # Generate LongSparseIntVect fingerprint...
 370         MolsFingerprints = [
 371             rdMolDescriptors.GetTopologicalTorsionFingerprint(Mol, includeChirality=UseChirality) for Mol in Mols
 372         ]
 373 
 374     return MolsFingerprints
 375 
 376 
 377 def GenerateLowerTriangularDistanceMatrix(MolsFingerprints):
 378     """Generate a lower triangular distance matrix without the diagonal."""
 379 
 380     SimilarityFunction = OptionsInfo["SimilarityFunction"]
 381 
 382     DistanceMatrix = []
 383     NumFPs = len(MolsFingerprints)
 384     for Index1 in range(0, NumFPs):
 385         for Index2 in range(0, Index1):
 386             Distance = 1 - SimilarityFunction(
 387                 MolsFingerprints[Index1],
 388                 MolsFingerprints[Index2],
 389             )
 390             DistanceMatrix.append(Distance)
 391 
 392     return DistanceMatrix
 393 
 394 
 395 def WriteMolecules(Mols):
 396     """Write out molecules."""
 397 
 398     Outfile = OptionsInfo["Outfile"]
 399 
 400     # Set up a molecule writer...
 401     Writer = None
 402     Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
 403     if Writer is None:
 404         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 405     MiscUtil.PrintInfo("\nGenerating file %s...\n" % Outfile)
 406 
 407     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 408     SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"]
 409 
 410     # Write out molecules...
 411     FirstMol = True
 412     for Mol in Mols:
 413         if FirstMol:
 414             FirstMol = False
 415             if SetSMILESMolProps:
 416                 RDKitUtil.SetWriterMolProps(Writer, Mol)
 417                 RDKitUtil.SetWriterMolProps(Writer, Mol)
 418 
 419         if Compute2DCoords:
 420             AllChem.Compute2DCoords(Mol)
 421         Writer.write(Mol)
 422 
 423     if Writer is not None:
 424         Writer.close()
 425 
 426     MiscUtil.PrintInfo("Total number of diverse molecules selected: %d" % (len(Mols)))
 427 
 428 
 429 def ProcessFingerprintsParameters():
 430     """Set up and process fingerprints parameters."""
 431 
 432     SetupFingerprintsNamesAndParameters()
 433     ProcessSpecifiedFingerprintsName()
 434     ProcessSpecifiedFingerprintsParameters()
 435 
 436 
 437 def SetupFingerprintsNamesAndParameters():
 438     """Set up fingerprints parameters."""
 439 
 440     OptionsInfo["FingerprintsNames"] = [
 441         "AtomPairs",
 442         "MACCS166Keys",
 443         "Morgan",
 444         "MorganFeatures",
 445         "PathLength",
 446         "TopologicalTorsions",
 447     ]
 448 
 449     OptionsInfo["FingerprintsParams"] = {}
 450     OptionsInfo["FingerprintsParams"]["AtomPairs"] = {"MinLength": 1, "MaxLength": 30, "UseChirality": False}
 451     OptionsInfo["FingerprintsParams"]["MACCS166Keys"] = {}
 452     OptionsInfo["FingerprintsParams"]["Morgan"] = {"Radius": 2, "UseChirality": False}
 453     OptionsInfo["FingerprintsParams"]["MorganFeatures"] = {"Radius": 2, "UseChirality": False}
 454     OptionsInfo["FingerprintsParams"]["TopologicalTorsions"] = {"UseChirality": False}
 455     OptionsInfo["FingerprintsParams"]["PathLength"] = {"MinPath": 1, "MaxPath": 7, "FPSize": 2048, "BitsPerHash": 2}
 456 
 457 
 458 def ProcessSpecifiedFingerprintsName():
 459     """Process specified fingerprints name."""
 460 
 461     #  Set up a canonical fingerprints name map...
 462     CanonicalFingerprintsNamesMap = {}
 463     for Name in OptionsInfo["FingerprintsNames"]:
 464         CanonicalName = Name.lower()
 465         CanonicalFingerprintsNamesMap[CanonicalName] = Name
 466 
 467     # Validate specified fingerprints name...
 468     CanonicalFingerprintsName = OptionsInfo["Fingerprints"].lower()
 469     if CanonicalFingerprintsName not in CanonicalFingerprintsNamesMap:
 470         MiscUtil.PrintError(
 471             'The fingerprints name, %s, specified using "-f, --fingerprints" option is not a valid name.'
 472             % (OptionsInfo["Fingerprints"])
 473         )
 474 
 475     OptionsInfo["SpecifiedFingerprints"] = CanonicalFingerprintsNamesMap[CanonicalFingerprintsName]
 476 
 477 
 478 def ProcessSpecifiedFingerprintsParameters():
 479     """Process specified fingerprints parameters."""
 480 
 481     if re.match("^auto$", OptionsInfo["ParamsFingerprints"], re.I):
 482         # Nothing to process...
 483         return
 484 
 485     SpecifiedFingerprintsName = OptionsInfo["SpecifiedFingerprints"]
 486 
 487     # Parse specified fingerprints parameters...
 488     ParamsFingerprints = re.sub(" ", "", OptionsInfo["ParamsFingerprints"])
 489     if not ParamsFingerprints:
 490         MiscUtil.PrintError(
 491             'No valid parameter name and value pairs specified using "-p, --paramsFingerprints" option corrresponding to fingerprints %s.'
 492             % (SpecifiedFingerprintsName)
 493         )
 494 
 495     ParamsFingerprintsWords = ParamsFingerprints.split(",")
 496     if len(ParamsFingerprintsWords) % 2:
 497         MiscUtil.PrintError(
 498             'The number of comma delimited paramater names and values, %d, specified using "-p, --paramsFingerprints" option must be an even number.'
 499             % (len(ParamsFingerprintsWords))
 500         )
 501 
 502     # Setup a canonical parameter names for specified fingerprints...
 503     ValidParamNames = []
 504     CanonicalParamNamesMap = {}
 505     for ParamName in sorted(OptionsInfo["FingerprintsParams"][SpecifiedFingerprintsName]):
 506         ValidParamNames.append(ParamName)
 507         CanonicalParamNamesMap[ParamName.lower()] = ParamName
 508 
 509     # Validate and set paramater names and value...
 510     for Index in range(0, len(ParamsFingerprintsWords), 2):
 511         Name = ParamsFingerprintsWords[Index]
 512         Value = ParamsFingerprintsWords[Index + 1]
 513 
 514         CanonicalName = Name.lower()
 515         if CanonicalName not in CanonicalParamNamesMap:
 516             MiscUtil.PrintError(
 517                 'The parameter name, %s, specified using "-p, --paramsFingerprints" option for fingerprints, %s, is not a valid name. Supported parameter names: %s'
 518                 % (Name, SpecifiedFingerprintsName, " ".join(ValidParamNames))
 519             )
 520 
 521         ParamName = CanonicalParamNamesMap[CanonicalName]
 522         if re.match("^UseChirality$", ParamName, re.I):
 523             if not re.match("^(Yes|No|True|False)$", Value, re.I):
 524                 MiscUtil.PrintError(
 525                     'The parameter value, %s, specified using "-p, --paramsFingerprints" option for fingerprints, %s, is not a valid value. Supported values: Yes No True False'
 526                     % (Value, SpecifiedFingerprintsName)
 527                 )
 528             ParamValue = False
 529             if re.match("^(Yes|True)$", Value, re.I):
 530                 ParamValue = True
 531         else:
 532             ParamValue = int(Value)
 533             if ParamValue <= 0:
 534                 MiscUtil.PrintError(
 535                     'The parameter value, %s, specified using "-p, --paramsFingerprints" option for fingerprints, %s, is not a valid value. Supported values: > 0'
 536                     % (Value, SpecifiedFingerprintsName)
 537                 )
 538 
 539         # Set value...
 540         OptionsInfo["FingerprintsParams"][SpecifiedFingerprintsName][ParamName] = ParamValue
 541 
 542 
 543 def ProcessSimilarityMetricParameter():
 544     """Process specified similarity metric value."""
 545 
 546     SimilarityInfoMap = {}
 547     CanonicalNameMap = {}
 548 
 549     for SimilarityFunctionInfo in DataStructs.similarityFunctions:
 550         Name = SimilarityFunctionInfo[0]
 551         Function = SimilarityFunctionInfo[1]
 552 
 553         SimilarityInfoMap[Name] = Function
 554         CanonicalName = Name.lower()
 555         CanonicalNameMap[CanonicalName] = Name
 556 
 557     SpecifiedCanonicalName = OptionsInfo["SimilarityMetric"].lower()
 558     SimilarityFunction = None
 559     if SpecifiedCanonicalName in CanonicalNameMap:
 560         SimilarityName = CanonicalNameMap[SpecifiedCanonicalName]
 561         SimilarityFunction = SimilarityInfoMap[SimilarityName]
 562     else:
 563         MiscUtil.PrintError("Similarity metric name, %s, is not a valid name. " % OptionsInfo["SimilarityMetric"])
 564 
 565     OptionsInfo["SimilarityMetric"] = SimilarityName
 566     OptionsInfo["SimilarityFunction"] = SimilarityFunction
 567 
 568     # RDKit similarity functions, besides Dice and Tanimoto, are not able to handle int bit vectors...
 569     GenerateBitVectFingerints = False
 570     if not re.match("^(Tanimoto|Dice)$", SimilarityName, re.I):
 571         GenerateBitVectFingerints = True
 572     OptionsInfo["GenerateBitVectFingerints"] = GenerateBitVectFingerints
 573 
 574 
 575 def ProcessClusteringMethodParameter():
 576     """Process specified clustering method parameter."""
 577 
 578     OptionsInfo["SpecifiedClusteringMethod"] = ""
 579     OptionsInfo["SpecifiedClusteringMethodID"] = ""
 580 
 581     if not re.match("^HierarchicalClustering$", OptionsInfo["Mode"], re.I):
 582         # Nothing to process...
 583         return
 584 
 585     # Setup a canonical cluster method name map..
 586     ClusteringMethodInfoMap = {}
 587     CanonicalClusteringMethodNameMap = {}
 588     for Name in sorted(rdSimDivPickers.ClusterMethod.names):
 589         NameID = rdSimDivPickers.ClusterMethod.names[Name]
 590         ClusteringMethodInfoMap[Name] = NameID
 591 
 592         CanonicalName = Name.lower()
 593         CanonicalClusteringMethodNameMap[CanonicalName] = Name
 594 
 595     CanonicalName = OptionsInfo["ClusteringMethod"].lower()
 596     if CanonicalName not in CanonicalClusteringMethodNameMap:
 597         MiscUtil.PrintError(
 598             'The cluster method, %s, specified using "-c, --clusteringMethod" option is not a valid name.'
 599             % (OptionsInfo["ClusteringMethod"])
 600         )
 601 
 602     SpecifiedClusteringMethodName = CanonicalClusteringMethodNameMap[CanonicalName]
 603     OptionsInfo["SpecifiedClusteringMethod"] = SpecifiedClusteringMethodName
 604     OptionsInfo["SpecifiedClusteringMethodID"] = ClusteringMethodInfoMap[SpecifiedClusteringMethodName]
 605 
 606 
 607 def ProcessOptions():
 608     """Process and validate command line arguments and options."""
 609 
 610     MiscUtil.PrintInfo("Processing options...")
 611 
 612     # Validate options...
 613     ValidateOptions()
 614 
 615     OptionsInfo["Mode"] = Options["--mode"]
 616     OptionsInfo["Fingerprints"] = Options["--fingerprints"]
 617 
 618     OptionsInfo["ClusteringMethod"] = Options["--clusteringMethod"]
 619     ProcessClusteringMethodParameter()
 620 
 621     OptionsInfo["NumMols"] = int(Options["--numMols"])
 622 
 623     OptionsInfo["Infile"] = Options["--infile"]
 624     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 625         "--infileParams", Options["--infileParams"], Options["--infile"]
 626     )
 627 
 628     OptionsInfo["Outfile"] = Options["--outfile"]
 629     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 630         "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"]
 631     )
 632 
 633     OptionsInfo["Overwrite"] = Options["--overwrite"]
 634 
 635     OptionsInfo["SimilarityMetric"] = Options["--similarityMetric"]
 636     ProcessSimilarityMetricParameter()
 637 
 638     OptionsInfo["ParamsFingerprints"] = Options["--paramsFingerprints"]
 639     ProcessFingerprintsParameters()
 640 
 641 
 642 def RetrieveOptions():
 643     """Retrieve command line arguments and options."""
 644 
 645     # Get options...
 646     global Options
 647     Options = docopt(_docoptUsage_)
 648 
 649     # Set current working directory to the specified directory...
 650     WorkingDir = Options["--workingdir"]
 651     if WorkingDir:
 652         os.chdir(WorkingDir)
 653 
 654     # Handle examples option...
 655     if "--examples" in Options and Options["--examples"]:
 656         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 657         sys.exit(0)
 658 
 659 
 660 def ValidateOptions():
 661     """Validate option values."""
 662 
 663     MiscUtil.ValidateOptionTextValue(
 664         "-c, --clusteringMethod", Options["--clusteringMethod"], "Centroid CLink Gower McQuitty SLink UPGMA Ward"
 665     )
 666     MiscUtil.ValidateOptionTextValue(
 667         "-f, --fingerprints",
 668         Options["--fingerprints"],
 669         "AtomPairs MACCS166Keys Morgan MorganFeatures PathLength TopologicalTorsions",
 670     )
 671 
 672     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "MaxMin HierarchicalClustering")
 673     MiscUtil.ValidateOptionIntegerValue("-n, --numMols", Options["--numMols"], {">": 0})
 674 
 675     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 676     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd smi txt csv tsv")
 677 
 678     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi")
 679     MiscUtil.ValidateOptionsOutputFileOverwrite(
 680         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 681     )
 682     MiscUtil.ValidateOptionsDistinctFileNames(
 683         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 684     )
 685 
 686     MiscUtil.ValidateOptionTextValue(
 687         "-s, --similarityMetric",
 688         Options["--similarityMetric"],
 689         "BraunBlanquet Cosine Dice Kulczynski RogotGoldberg Russel Sokal Tanimoto",
 690     )
 691 
 692 
 693 # Setup a usage string for docopt...
 694 _docoptUsage_ = """
 695 RDKitPickDiverseMolecules.py - Pick a diverse subset of molecules
 696 
 697 Usage:
 698     RDKitPickDiverseMolecules.py [--clusteringMethod <Centroid, CLink...>]
 699                                  [--fingerprints <MACCS166Keys, Morgan, PathLength...>]
 700                                  [--infileParams <Name,Value,...>] [--mode <MaxMin or HierarchicalClustering>]
 701                                  [--numMols <number>]  [--outfileParams <Name,Value,...>] 
 702                                  [--overwrite] [--paramsFingerprints <Name,Value,...>]
 703                                  [--similarityMetric <Dice, Tanimoto...>] [-w <dir>] -i <infile> -o <outfile> 
 704     RDKitPickDiverseMolecules.py -h | --help | -e | --examples
 705 
 706 Description:
 707     Pick a subset of diverse molecules  based on a variety of 2D fingerprints using
 708     MaxMin [ Ref 135 ] or an available hierarchical clustering methodology and write
 709     them to a file.
 710 
 711     The default fingerprints types for various fingerprints are shown below:
 712 
 713         AtomPairs              IntSparseIntVect
 714         MACCS166Keys           ExplicitBitVect
 715         Morgan                 UIntSparseIntVect
 716         MorganFeatures         UIntSparseIntVect
 717         PathLength             ExplicitBitVect
 718         TopologicalTorsions    LongSparseIntVect
 719  
 720     The Dice and Tanimoto similarity functions available in RDKit are able to
 721     handle fingerprints corresponding to both IntVect and BitVect. All other
 722     similarity functions, however, expect BitVect fingerprints to calculate
 723     pairwise similarity. Consequently, ExplicitBitVect fingerprints are generated
 724     for AtomPairs, Morgan, MorganFeatures, and TopologicalTorsions for
 725     similarity calculations instead of default IntVect fingerprints.
 726 
 727     The supported input file formats are: SD (.sdf, .sd), SMILES (.smi, .csv, .tsv, .txt)
 728 
 729     The supported output file formats are: SD (.sdf, .sd), SMILES (.smi)
 730 
 731 Options:
 732     -c, --clusteringMethod <Centroid, CLink...>  [default: Centroid]
 733         Clustering method to use for picking a subset of diverse molecules during
 734         hierarchical clustering. Supported values: Centroid, CLink, Gower,
 735         McQuitty, SLink, UPGMA, Ward. This option is ignored for 'MaxMin' value
 736         of '-m, --mode' option. The Clink and SLink corresponding to CompleteLink
 737         and SingleLink cluster method.
 738     -f, --fingerprints <MACCS166Keys, Morgan, PathLength...>  [default: Morgan]
 739         Fingerprints to use for calculating similarity/distance between molecules.
 740         Supported values: AtomPairs, MACCS166Keys, Morgan, MorganFeatures, PathLength,
 741         TopologicalTorsions. The PathLength fingerprints are Daylight like fingerprints.
 742         The Morgan and MorganFeature fingerprints are circular fingerprints, corresponding
 743         Scitegic's Extended Connectivity Fingerprints (ECFP) and Features Connectivity
 744         Fingerprints (FCFP). The values of default parameters for generating fingerprints
 745         can be modified using '-p, --paramsFingerprints' option.
 746     -e, --examples
 747         Print examples.
 748     -h, --help
 749         Print this help message.
 750     -i, --infile <infile>
 751         Input file name.
 752     --infileParams <Name,Value,...>  [default: auto]
 753         A comma delimited list of parameter name and value pairs for reading
 754         molecules from files. The supported parameter names for different file
 755         formats, along with their default values, are shown below:
 756             
 757             SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
 758             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 759                 smilesTitleLine,auto,sanitize,yes
 760             
 761         Possible values for smilesDelimiter: space, comma or tab.
 762     -m, --mode <MaxMin or HierarchicalClustering>  [default: MaxMin]
 763         Pick a diverse subset of molecules using MaxMin or hierarchical clustering
 764         methodology.
 765     -n, --numMols <number>  [default: 25]
 766         Number of diverse molecules to pick.
 767     -o, --outfile <outfile>
 768         Output file name.
 769     --outfileParams <Name,Value,...>  [default: auto]
 770         A comma delimited list of parameter name and value pairs for writing
 771         molecules to files. The supported parameter names for different file
 772         formats, along with their default values, are shown below:
 773             
 774             SD: compute2DCoords,auto,kekulize,yes,forceV3000,no
 775             SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes,
 776                 smilesTitleLine,yes,smilesMolName,yes,smilesMolProps,no
 777             
 778         Default value for compute2DCoords: yes for SMILES input file; no for all other
 779         file types.
 780     --overwrite
 781         Overwrite existing files.
 782     -p, --paramsFingerprints <Name,Value,...>  [default: auto]
 783         Parameter values to use for generating fingerprints. The default values
 784         are dependent on the value of '-f, --fingerprints' option. In general, it is a
 785         comma delimited list of parameter name and value pairs for the name of
 786         the fingerprints specified using '-f, --fingerprints' option. The supported
 787         parameter names along with their default values for valid fingerprints
 788         names are shown below:
 789             
 790             AtomPairs: minLength,1 ,maxLength,30, useChirality,No
 791             Morgan:   radius,2, useChirality,No
 792             MorganFeatures:   radius,2, useChirality,No
 793             PathLength: minPath,1, maxPath,7, fpSize, 2048, bitsPerHash,2
 794             TopologicalTorsions: useChirality,No
 795             
 796     -s, --similarityMetric <Dice, Tanimoto...>  [default: Tanimoto]
 797         Similarity metric to use for calculating similarity/distance between molecules.
 798         Possible values: BraunBlanquet, Cosine, Dice, Kulczynski, RogotGoldberg,
 799         Russel, Sokal, Tanimoto.
 800     -w, --workingdir <dir>
 801         Location of working directory which defaults to the current directory.
 802 
 803 Examples:
 804     To pick 25 diverse molecules using MaxMin methodology, Tanimoto similarity
 805     metric corresponding to Morgan fingerprints with radius of 2, and write
 806     out a SMILES file, type:
 807 
 808         % RDKitPickDiverseMolecules.py  -i Sample.smi -o SampleOut.smi
 809 
 810     To pick 50 diverse molecules using MaxMin methodology, Dice similarity metric
 811     corresponding to PathLength fingerprints with max path length of 6, and write
 812     out a SD file, type:
 813 
 814         % RDKitPickDiverseMolecules.py  -m MaxMin -f PathLength -s Dice -n 50
 815           -p 'maxPath,6' -i Sample.sdf -o SampleOut.sdf
 816 
 817     To pick 25 diverse molecules using Centroid hierarchical clustering methodology,
 818     Tanimoto similarity metric corresponding to Morgan fingerprints with radius of 2,
 819     and write out a SMILES file, type:
 820 
 821         % RDKitPickDiverseMolecules.py  -m HierarchicalClustering -i Sample.smi
 822           -o SampleOut.smi
 823 
 824     To pick 50 diverse molecules using Ward hierarchical methodology methodology,
 825     Dice similarity metric corresponding to MorganFeatures fingerprints with radius
 826     of 2 along with deploying chirality, and write out a SD file, type:
 827 
 828         % RDKitPickDiverseMolecules.py  -m HierarchicalClustering -c Ward -n 50
 829           -f MorganFeatures -p 'radius,2,useChirality,No' -i Sample.sdf -o
 830           SampleOut.sdf
 831 
 832     To pick 25 diverse molecules using MaxMin methodology, Tanimoto similarity
 833     metric corresponding to Morgan fingerprints with radius of 2 from a CSV SMIKES
 834     file , SMILES strings in column 1, name in olumn 2, and write out a SD file, type:
 835 
 836         % RDKitPickDiverseMolecules.py  --infileParams
 837           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
 838           smilesNameColumn,2" --outfileParams "compute2DCoords,yes"
 839           -i SampleSMILES.csv -o SampleOut.sdf
 840 
 841 Author:
 842     Manish Sud(msud@san.rr.com)
 843 
 844 See also:
 845     RDKitClusterMolecules.py, RDKitConvertFileFormat.py, RDKitSearchFunctionalGroups.py,
 846     RDKitSearchSMARTS.py
 847 
 848 Copyright:
 849     Copyright (C) 2026 Manish Sud. All rights reserved.
 850 
 851     The functionality available in this script is implemented using RDKit, an
 852     open source toolkit for cheminformatics developed by Greg Landrum.
 853 
 854     This file is part of MayaChemTools.
 855 
 856     MayaChemTools is free software; you can redistribute it and/or modify it under
 857     the terms of the GNU Lesser General Public License as published by the Free
 858     Software Foundation; either version 3 of the License, or (at your option) any
 859     later version.
 860 
 861 """
 862 
 863 if __name__ == "__main__":
 864     main()