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()