MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: RDKitEnumerateCompoundLibrary.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 except ImportError as ErrMsg:
  42     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
  43     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
  44     sys.exit(1)
  45 
  46 # MayaChemTools imports...
  47 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  48 try:
  49     from docopt import docopt
  50     import MiscUtil
  51     import RDKitUtil
  52 except ImportError as ErrMsg:
  53     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  54     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  55     sys.exit(1)
  56 
  57 ScriptName = os.path.basename(sys.argv[0])
  58 Options = {}
  59 OptionsInfo = {}
  60 
  61 RxnNamesMap = {}
  62 
  63 
  64 def main():
  65     """Start execution of the script."""
  66 
  67     MiscUtil.PrintInfo(
  68         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
  69         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
  70     )
  71 
  72     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  73 
  74     # Retrieve command line arguments and options...
  75     RetrieveOptions()
  76 
  77     # Process and validate command line arguments and options...
  78     ProcessOptions()
  79 
  80     # Perform actions required by the script...
  81     PerformChemicalLibraryEnumeration()
  82 
  83     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  84     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  85 
  86 
  87 def PerformChemicalLibraryEnumeration():
  88     """Retrieve functional groups information and perform search."""
  89 
  90     ProcessReactionNamesInfo()
  91     PerformEnumeration()
  92 
  93 
  94 def PerformEnumeration():
  95     """Enumerate virutal compound library."""
  96 
  97     ReactantFilesList = OptionsInfo["ReactantFilesList"]
  98     Outfile = OptionsInfo["Outfile"]
  99 
 100     RxnByNameMode = OptionsInfo["RxnByNameMode"]
 101     if RxnByNameMode:
 102         RxnSMARTSPattern = OptionsInfo["RxnNameSMARTS"]
 103     else:
 104         RxnSMARTSPattern = OptionsInfo["SpecifiedSMARTS"]
 105 
 106     # Set up a reaction and match number of reactants in rxn SMARTS against number of
 107     # reactant files...
 108     MiscUtil.PrintInfo("\nValidating reaction SMARTS...")
 109     try:
 110         Rxn = AllChem.ReactionFromSmarts(RxnSMARTSPattern)
 111     except Exception as ErrMsg:
 112         MiscUtil.PrintError("Failed to validate reaction SMARTS %s\n%s\n" % (RxnSMARTSPattern, ErrMsg))
 113 
 114     RxnReactantsCount = Rxn.GetNumReactantTemplates()
 115 
 116     ReactantFilesList = OptionsInfo["ReactantFilesList"]
 117     ReactantFilesCount = len(ReactantFilesList)
 118     if ReactantFilesCount != RxnReactantsCount:
 119         MiscUtil.PrintError(
 120             "The number of specified reactant files, %d, must match number of reactants, %d, in reaction SMARTS"
 121             % (ReactantFilesCount, RxnReactantsCount)
 122         )
 123 
 124     # Retrieve reactant molecules...
 125     ReactantsMolsList = RetrieveReactantsMolecules()
 126 
 127     # Set up  a molecule writer...
 128     Writer = None
 129     Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
 130     if Writer is None:
 131         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 132 
 133     MiscUtil.PrintInfo("\nGenerating file %s..." % Outfile)
 134 
 135     # Set up reaction...
 136     ReturnReactants = False
 137     if OptionsInfo["UseReactantNames"]:
 138         ReturnReactants = True
 139     RxnProducts = AllChem.EnumerateLibraryFromReaction(Rxn, ReactantsMolsList, ReturnReactants)
 140 
 141     # Generate product molecules and write them out...
 142 
 143     Compute2DCoords = OptionsInfo["Compute2DCoords"]
 144     Sanitize = OptionsInfo["Sanitize"]
 145 
 146     ProdMolCount = 0
 147     ValidProdMolCount = 0
 148 
 149     if ReturnReactants:
 150         for Products, Reactants in list(RxnProducts):
 151             for ProdMol in Products:
 152                 ProdMolCount += 1
 153 
 154                 # Set product name...
 155                 ReactantMolNames = [ReactantMol.GetProp("_Name") for ReactantMol in Reactants]
 156                 Delimiter = "_"
 157                 ProdMolName = Delimiter.join(ReactantMolNames) + "_Prod%d" % ProdMolCount
 158                 ProdMol.SetProp("_Name", ProdMolName)
 159 
 160                 Status = WriteProductMolecule(Writer, ProdMol, Sanitize, Compute2DCoords)
 161                 if Status:
 162                     ValidProdMolCount += 1
 163     else:
 164         for Products in list(RxnProducts):
 165             for ProdMol in Products:
 166                 ProdMolCount += 1
 167 
 168                 # Set product name...
 169                 ProdMolName = "Prod%d" % ProdMolCount
 170                 ProdMol.SetProp("_Name", ProdMolName)
 171 
 172                 Status = WriteProductMolecule(Writer, ProdMol, Sanitize, Compute2DCoords)
 173                 if Status:
 174                     ValidProdMolCount += 1
 175 
 176     if Writer is not None:
 177         Writer.close()
 178 
 179     if ValidProdMolCount:
 180         MiscUtil.PrintInfo("\nTotal number of product molecules: %d" % ProdMolCount)
 181         MiscUtil.PrintInfo("Number of valid product molecules: %d" % ValidProdMolCount)
 182         MiscUtil.PrintInfo("Number of ignored product molecules: %d" % (ProdMolCount - ValidProdMolCount))
 183     else:
 184         MiscUtil.PrintInfo(
 185             "\nThe compound library enumeration failed to generate any product molecules.\nCheck to make sure the reactants specified in input files match their corresponding specifications in reaction SMARTS and try again."
 186         )
 187 
 188 
 189 def WriteProductMolecule(Writer, ProdMol, Sanitize, Compute2DCoords):
 190     """Prepare and write out product  molecule."""
 191 
 192     try:
 193         if Sanitize:
 194             Chem.SanitizeMol(ProdMol)
 195     except (RuntimeError, ValueError):
 196         MiscUtil.PrintWarning("Ignoring product molecule: Failed to sanitize...\n")
 197         return False
 198 
 199     try:
 200         if Compute2DCoords:
 201             AllChem.Compute2DCoords(ProdMol)
 202     except (RuntimeError, ValueError):
 203         MiscUtil.PrintWarning("Ignoring product molecule: Failed to compute 2D coordinates...\n")
 204         return False
 205 
 206     Writer.write(ProdMol)
 207 
 208     return True
 209 
 210 
 211 def RetrieveReactantsMolecules():
 212     """Retrieve reactant molecules from each reactant file and return a list containing lists of molecules
 213     for each reactant file."""
 214 
 215     MiscUtil.PrintInfo("\nProcessing reactant file(s)...")
 216 
 217     ReactantsMolsList = []
 218     ReactantFilesList = OptionsInfo["ReactantFilesList"]
 219     UseReactantNames = OptionsInfo["UseReactantNames"]
 220     ReactantCount = 0
 221 
 222     for FileIndex in range(0, len(ReactantFilesList)):
 223         ReactantCount += 1
 224         ReactantFile = ReactantFilesList[FileIndex]
 225 
 226         MiscUtil.PrintInfo("\nProcessing reactant file: %s..." % ReactantFile)
 227 
 228         Mols = RDKitUtil.ReadMolecules(ReactantFile, **OptionsInfo["InfileParams"])
 229 
 230         ValidMols = []
 231         MolCount = 0
 232         ValidMolCount = 0
 233 
 234         for Mol in Mols:
 235             MolCount += 1
 236             if Mol is None:
 237                 continue
 238 
 239             if RDKitUtil.IsMolEmpty(Mol):
 240                 MolName = RDKitUtil.GetMolName(Mol, MolCount)
 241                 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 242                 continue
 243 
 244             ValidMolCount += 1
 245 
 246             # Check and set mol name...
 247             if UseReactantNames:
 248                 MolName = RDKitUtil.GetMolName(Mol)
 249                 if not len(MolName):
 250                     MolName = "React%dMol%d" % (ReactantCount, MolCount)
 251                     Mol.SetProp("_Name", MolName)
 252 
 253             ValidMols.append(Mol)
 254 
 255         ReactantsMolsList.append(ValidMols)
 256 
 257         MiscUtil.PrintInfo("Total number of molecules: %d" % MolCount)
 258         MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
 259         MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
 260 
 261     return ReactantsMolsList
 262 
 263 
 264 def ProcessReactionNamesInfo():
 265     """Process reaction names information."""
 266 
 267     if not OptionsInfo["RxnByNameMode"]:
 268         return
 269 
 270     RetrieveReactionNamesInfo()
 271     ProcessSpecifiedReactionName()
 272 
 273 
 274 def ProcessSpecifiedReactionName():
 275     """Process and validate specified reaction name."""
 276 
 277     OptionsInfo["RxnNameSMARTS"] = None
 278 
 279     # Set up a map of valid group rxn names for checking specified rxn names...
 280     CanonicalRxnNameMap = {}
 281     for Name in RxnNamesMap["Names"]:
 282         CanonicalRxnNameMap[Name.lower()] = Name
 283 
 284     CanonicalRxnName = OptionsInfo["RxnName"].lower()
 285     if CanonicalRxnName in CanonicalRxnNameMap:
 286         Name = CanonicalRxnNameMap[CanonicalRxnName]
 287         OptionsInfo["RxnNameSMARTS"] = RxnNamesMap["SMARTSPattern"][Name]
 288     else:
 289         MiscUtil.PrintError(
 290             'The rxn name name, %s, specified using "-r, --rxnName" option is not a valid name.'
 291             % (OptionsInfo["RxnName"])
 292         )
 293 
 294 
 295 def ProcessListReactionNamesOption():
 296     """Process list reaction names information."""
 297 
 298     ProcessReactionNamesFileOption()
 299     ProcessColumnOptions()
 300 
 301     RetrieveReactionNamesInfo()
 302     ListAndValidateReactionNamesInfo()
 303 
 304 
 305 def RetrieveReactionNamesInfo():
 306     """Retrieve reaction names information."""
 307 
 308     RxnNamesFile = OptionsInfo["RxnNamesFile"]
 309 
 310     MiscUtil.PrintInfo("\nRetrieving reaction names and SMARTS patterns from file %s" % (RxnNamesFile))
 311 
 312     if not os.path.exists(RxnNamesFile):
 313         MiscUtil.PrintError("The reaction names file, %s, doesn't exist.\n" % (RxnNamesFile))
 314 
 315     IgnoreHeaderLine = True
 316     RxnLinesWords = MiscUtil.GetTextLinesWords(
 317         RxnNamesFile, OptionsInfo["RxnNamesFileDelimiter"], OptionsInfo["RxnNamesFileQuote"], IgnoreHeaderLine
 318     )
 319 
 320     RxnNamesMap["Names"] = []
 321     RxnNamesMap["SMARTSPattern"] = {}
 322 
 323     RxnNameColindex = OptionsInfo["RxnNameColnum"] - 1
 324     RxnSMARTSColindex = OptionsInfo["RxnSMARTSColnum"] - 1
 325 
 326     for LineWords in RxnLinesWords:
 327         Name = LineWords[RxnNameColindex]
 328         SMARTSPattern = LineWords[RxnSMARTSColindex]
 329 
 330         if Name in RxnNamesMap["SMARTSPattern"]:
 331             MiscUtil.PrintWarning("Ignoring duplicate reaction name: %s..." % Name)
 332         else:
 333             RxnNamesMap["Names"].append(Name)
 334             RxnNamesMap["SMARTSPattern"][Name] = SMARTSPattern
 335 
 336     if not len(RxnNamesMap["Names"]):
 337         MiscUtil.PrintError("Failed to retrieve any reaction names and SMARTS patterns...")
 338 
 339     MiscUtil.PrintInfo(
 340         "Total number of reactions present in reaction names and SMARTS file: %d" % (len(RxnNamesMap["Names"]))
 341     )
 342 
 343 
 344 def ListAndValidateReactionNamesInfo():
 345     """List and validate reaction names information."""
 346 
 347     ListReactionNamesInfo()
 348     ValidateReactionNamesInfo()
 349 
 350 
 351 def ListReactionNamesInfo():
 352     """List reaction names information."""
 353 
 354     MiscUtil.PrintInfo("\nListing available reaction names and SMARTS patterns...")
 355     MiscUtil.PrintInfo("\nReactionName\tSMARTSPattern")
 356 
 357     RxnCount = 0
 358     for Name in sorted(RxnNamesMap["Names"]):
 359         RxnCount += 1
 360         SMARTSPattern = RxnNamesMap["SMARTSPattern"][Name]
 361         MiscUtil.PrintInfo("%s\t%s" % (Name, SMARTSPattern))
 362 
 363     MiscUtil.PrintInfo("\nTotal number of reactions: %s" % RxnCount)
 364 
 365 
 366 def ValidateReactionNamesInfo():
 367     """Validate reaction names information."""
 368 
 369     MiscUtil.PrintInfo("\nValidating reaction SMARTS patterns...")
 370 
 371     RxnCount = 0
 372     ValidRxnCount = 0
 373     for Name in sorted(RxnNamesMap["Names"]):
 374         RxnCount += 1
 375         SMARTSPattern = RxnNamesMap["SMARTSPattern"][Name]
 376         try:
 377             AllChem.ReactionFromSmarts(SMARTSPattern)
 378             ValidRxnCount += 1
 379         except Exception as ErrMsg:
 380             MiscUtil.PrintInfo(
 381                 "\nFailed to validate reaction SMARTS. ReactionName: %s;  SMARTSPattern: %s\n%s\n"
 382                 % (Name, SMARTSPattern, ErrMsg)
 383             )
 384 
 385     InvalidRxnCount = RxnCount - ValidRxnCount
 386     MiscUtil.PrintInfo(
 387         "\nTotal number of reactions: %s\nNumber of valid reactions: %s\nNumber of invalid reactions: %s"
 388         % (RxnCount, ValidRxnCount, InvalidRxnCount)
 389     )
 390 
 391     MiscUtil.PrintInfo("")
 392 
 393 
 394 def ProcessReactionNamesFileOption():
 395     """Process reaction names file option."""
 396 
 397     RxnNamesFile = None
 398     if not re.match("^auto$", Options["--rxnNamesFile"], re.I):
 399         MiscUtil.ValidateOptionFilePath("--rxnNamesFile", Options["--rxnNamesFile"])
 400         RxnNamesFile = Options["--rxnNamesFile"]
 401 
 402     if RxnNamesFile is None:
 403         MayaChemToolsDataDir = MiscUtil.GetMayaChemToolsLibDataPath()
 404         RxnNamesFile = os.path.join(MayaChemToolsDataDir, "ReactionNamesAndSMARTS.csv")
 405 
 406     OptionsInfo["RxnNamesFile"] = RxnNamesFile
 407     OptionsInfo["RxnNamesFileDelimiter"] = ","
 408     OptionsInfo["RxnNamesFileQuote"] = '"'
 409 
 410 
 411 def ProcessColumnOptions():
 412     """Process column options."""
 413 
 414     ProcessColumnModeOption()
 415     RetrieveColumnNames()
 416 
 417     ProcessReactionNameColOption()
 418     ProcessReactionSMARTSColOption()
 419 
 420 
 421 def ProcessColumnModeOption():
 422     """Process column mode option."""
 423 
 424     CollabelMode, ColnumMode = [False, False]
 425     Colmode = Options["--colmode"]
 426     if re.match("^collabel$", Colmode, re.I):
 427         CollabelMode = True
 428     elif re.match("^colnum$", Colmode, re.I):
 429         ColnumMode = True
 430     else:
 431         MiscUtil.PrintError(
 432             'The value, %s, specified for option "-c, --colmode" is not valid. Supported values: collabel or colnum\n'
 433             % (Colmode)
 434         )
 435 
 436     OptionsInfo["Colmode"] = Colmode
 437     OptionsInfo["CollabelMode"] = CollabelMode
 438     OptionsInfo["ColnumMode"] = ColnumMode
 439 
 440 
 441 def RetrieveColumnNames():
 442     """Retrieve column names."""
 443 
 444     RxnNamesFile = OptionsInfo["RxnNamesFile"]
 445     IgnoreHeaderLine = False
 446     RxnLinesWords = MiscUtil.GetTextLinesWords(
 447         RxnNamesFile, OptionsInfo["RxnNamesFileDelimiter"], OptionsInfo["RxnNamesFileQuote"], IgnoreHeaderLine
 448     )
 449     Colnames = RxnLinesWords[0]
 450 
 451     if len(Colnames) == 0:
 452         MiscUtil.PrintError(
 453             "The first line in reaction names, %s, is empty. It must contain column names.\n"
 454             % OptionsInfo["RxnNamesFile"]
 455         )
 456 
 457     ColnameToColnumMap = {}
 458     ColnumToColnameMap = {}
 459     for ColIndex, Colname in enumerate(Colnames):
 460         Colnum = ColIndex + 1
 461         ColnameToColnumMap[Colname] = Colnum
 462         ColnumToColnameMap[Colnum] = Colname
 463 
 464     OptionsInfo["Colnames"] = Colnames
 465     OptionsInfo["ColCount"] = len(Colnames)
 466     OptionsInfo["ColnameToColnumMap"] = ColnameToColnumMap
 467     OptionsInfo["ColnumToColnameMap"] = ColnumToColnameMap
 468 
 469 
 470 def ProcessReactionNameColOption():
 471     """Process reaction name column option."""
 472 
 473     RxnNameCol = Options["--colRxnName"]
 474     if re.match("^auto$", RxnNameCol, re.I):
 475         Colname = "RxnName"
 476         if Colname not in OptionsInfo["ColnameToColnumMap"]:
 477             MiscUtil.PrintError(
 478                 'The reaction name column name, %s, doen\'t exist in reaction names file. You must specify a valid reaction name column name or number using "--colRxnName" option.\n'
 479                 % Colname
 480             )
 481 
 482         Colnum = OptionsInfo["ColnameToColnumMap"][Colname]
 483         RxnNameColSpec = Colnum if OptionsInfo["ColnumMode"] else Colname
 484     else:
 485         RxnNameColSpec = RxnNameCol
 486 
 487     RxnNameColname, RxnNameColnum = ProcessColumnSpecification("--colRxnName", RxnNameColSpec)
 488 
 489     OptionsInfo["RxnNameCol"] = RxnNameCol
 490     OptionsInfo["RxnNameColname"] = RxnNameColname
 491     OptionsInfo["RxnNameColnum"] = RxnNameColnum
 492 
 493 
 494 def ProcessReactionSMARTSColOption():
 495     """Process reaction SMARTS column option."""
 496 
 497     RxnSMARTSCol = Options["--colRxnSMARTS"]
 498     if re.match("^auto$", RxnSMARTSCol, re.I):
 499         Colname = "RxnSMARTS"
 500         if Colname not in OptionsInfo["ColnameToColnumMap"]:
 501             MiscUtil.PrintError(
 502                 'The reaction SMARTS column name, %s, doen\'t exist in reaction names file. You must specify a valid reaction name column name or number using "--colRxnSMARTS" option.\n'
 503                 % Colname
 504             )
 505 
 506         Colnum = OptionsInfo["ColnameToColnumMap"][Colname]
 507         RxnSMARTSColSpec = Colnum if OptionsInfo["ColnumMode"] else Colname
 508     else:
 509         RxnSMARTSColSpec = RxnSMARTSCol
 510 
 511     RxnSMARTSColname, RxnSMARTSColnum = ProcessColumnSpecification("--colRxnSMARTS", RxnSMARTSColSpec)
 512 
 513     OptionsInfo["RxnSMARTSCol"] = RxnSMARTSCol
 514     OptionsInfo["RxnSMARTSColname"] = RxnSMARTSColname
 515     OptionsInfo["RxnSMARTSColnum"] = RxnSMARTSColnum
 516 
 517 
 518 def ProcessColumnSpecification(OptionName, Colspec):
 519     """Process column specification corresponding to a column name or number."""
 520 
 521     Colname, Colnum = [None, None]
 522     if OptionsInfo["ColnumMode"]:
 523         Colnum = int(Colspec)
 524         if Colnum not in OptionsInfo["ColnumToColnameMap"]:
 525             MiscUtil.PrintError(
 526                 'The column number, %s, specified using "%s" option doesn\'t exist in reaction names file. You must specify a valid column number. Valid values: >= 1 and <= %s\n'
 527                 % (Colnum, OptionName, OptionsInfo["ColCount"])
 528             )
 529         Colname = OptionsInfo["ColnumToColnameMap"][Colnum]
 530     else:
 531         Colname = Colspec
 532         if Colname not in OptionsInfo["ColnameToColnumMap"]:
 533             MiscUtil.PrintError(
 534                 'The column name, %s, specified using "%s" option doesn\'t exist in input file. You must specify a valid column name. Valid values: %s\n'
 535                 % (Colname, OptionName, " ".join(OptionsInfo["Colnames"]))
 536             )
 537         Colnum = OptionsInfo["ColnameToColnumMap"][Colname]
 538 
 539     return (Colname, Colnum)
 540 
 541 
 542 def ProcessOptions():
 543     """Process and validate command line arguments and options."""
 544 
 545     MiscUtil.PrintInfo("Processing options...")
 546 
 547     # Validate options...
 548     ValidateOptions()
 549 
 550     Compute2DCoords = True
 551     if not re.match("^yes$", Options["--compute2DCoords"], re.I):
 552         Compute2DCoords = False
 553     OptionsInfo["Compute2DCoords"] = Compute2DCoords
 554 
 555     OptionsInfo["Mode"] = Options["--mode"]
 556     RxnByNameMode = True
 557     if not re.match("^RxnByName$", Options["--mode"], re.I):
 558         RxnByNameMode = False
 559     OptionsInfo["RxnByNameMode"] = RxnByNameMode
 560 
 561     OptionsInfo["ProdMolNamesMode"] = Options["--prodMolNames"]
 562     UseReactantNames = False
 563     if re.match("^UseReactants$", Options["--prodMolNames"], re.I):
 564         UseReactantNames = True
 565     OptionsInfo["UseReactantNames"] = UseReactantNames
 566 
 567     OptionsInfo["RxnName"] = Options["--rxnName"]
 568     OptionsInfo["RxnNameSMARTS"] = None
 569     if OptionsInfo["RxnByNameMode"]:
 570         if not Options["--rxnName"]:
 571             MiscUtil.PrintError(
 572                 'No rxn name specified using "-r, --rxnName" option during "RxnByName" value of "-m, --mode" option'
 573             )
 574 
 575     ProcessReactionNamesFileOption()
 576     ProcessColumnOptions()
 577 
 578     ReactantFiles = re.sub(" ", "", Options["--infiles"])
 579     ReactantFilesList = []
 580     ReactantFilesList = ReactantFiles.split(",")
 581     OptionsInfo["ReactantFiles"] = ReactantFiles
 582     OptionsInfo["ReactantFilesList"] = ReactantFilesList
 583 
 584     OptionsInfo["SpecifiedSMARTS"] = Options["--smartsRxn"]
 585     if not OptionsInfo["RxnByNameMode"]:
 586         if not Options["--smartsRxn"]:
 587             MiscUtil.PrintError(
 588                 'No rxn SMARTS pattern specified using "-r, --rxnName" option during "RxnByName" value of "-m, --mode" option'
 589             )
 590 
 591     OptionsInfo["Outfile"] = Options["--outfile"]
 592     OptionsInfo["Overwrite"] = Options["--overwrite"]
 593 
 594     # Use first reactant file as input file as all input files have the same format...
 595     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 596         "--infileParams", Options["--infileParams"], ReactantFilesList[0]
 597     )
 598 
 599     # No need to pass any input or output file name due to absence of any auto parameter...
 600     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 601         "--outfileParams", Options["--outfileParams"]
 602     )
 603 
 604     Sanitize = True
 605     if not re.match("^yes$", Options["--sanitize"], re.I):
 606         Sanitize = False
 607     OptionsInfo["Sanitize"] = Sanitize
 608 
 609 
 610 def RetrieveOptions():
 611     """Retrieve command line arguments and options."""
 612 
 613     # Get options...
 614     global Options
 615     Options = docopt(_docoptUsage_)
 616 
 617     # Set current working directory to the specified directory...
 618     WorkingDir = Options["--workingdir"]
 619     if WorkingDir:
 620         os.chdir(WorkingDir)
 621 
 622     # Handle examples option...
 623     if "--examples" in Options and Options["--examples"]:
 624         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 625         sys.exit(0)
 626 
 627     # Handle listing of functional group information...
 628     if Options and Options["--list"]:
 629         ProcessListReactionNamesOption()
 630         sys.exit(0)
 631 
 632 
 633 def ValidateOptions():
 634     """Validate option values."""
 635 
 636     MiscUtil.ValidateOptionTextValue("-c, --colmode", Options["--colmode"], "collabel colnum")
 637 
 638     MiscUtil.ValidateOptionTextValue("--compute2DCoords", Options["--compute2DCoords"], "yes no")
 639 
 640     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "RxnByName RxnBySMARTS")
 641     MiscUtil.ValidateOptionTextValue("-p, --prodMolNames", Options["--prodMolNames"], "UseReactants Sequential")
 642 
 643     if not re.match("^auto$", Options["--rxnNamesFile"], re.I):
 644         MiscUtil.ValidateOptionFilePath("--rxnNamesFile", Options["--rxnNamesFile"])
 645 
 646     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi")
 647     MiscUtil.ValidateOptionsOutputFileOverwrite(
 648         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 649     )
 650 
 651     ReactantFiles = re.sub(" ", "", Options["--infiles"])
 652     if not ReactantFiles:
 653         MiscUtil.PrintError('No reactant files specified for "-i, --infiles" option')
 654 
 655     # Validate file extensions...
 656     for ReactantFile in ReactantFiles.split(","):
 657         MiscUtil.ValidateOptionFilePath("-i, --infiles", ReactantFile)
 658         MiscUtil.ValidateOptionFileExt("-i, --infiles", ReactantFile, "sdf sd smi csv tsv txt")
 659         MiscUtil.ValidateOptionsDistinctFileNames("-i, --infiles", ReactantFile, "-o, --outfile", Options["--outfile"])
 660 
 661     # Match file formats...
 662     FirstFile = True
 663     FirstFileFormat = ""
 664     for ReactantFile in ReactantFiles.split(","):
 665         FileFormat = ""
 666         if MiscUtil.CheckFileExt(ReactantFile, "sdf sd"):
 667             FileFormat = "SD"
 668         elif MiscUtil.CheckFileExt(ReactantFile, "smi csv tsv txt"):
 669             FileFormat = "SMILES"
 670         else:
 671             MiscUtil.PrintError(
 672                 'The file name specified , %s, for option "-i, --infiles" is not valid. Supported file formats: sdf sd smi csv tsv txt\n'
 673                 % ReactantFile
 674             )
 675 
 676         if FirstFile:
 677             FirstFile = False
 678             FirstFileFormat = FileFormat
 679             continue
 680 
 681         if not re.match("^%s$" % FirstFileFormat, FileFormat, re.IGNORECASE):
 682             MiscUtil.PrintError(
 683                 'All reactant file names -  %s - specified using option "-i, --infiles" must have the same file format.\n'
 684                 % ReactantFiles
 685             )
 686 
 687     MiscUtil.ValidateOptionTextValue("--sanitize", Options["--sanitize"], "yes no")
 688 
 689 
 690 # Setup a usage string for docopt...
 691 _docoptUsage_ = """
 692 RDKitEnumerateCompoundLibrary.py - Enumerate a virtual compound library
 693 
 694 Usage:
 695     RDKitEnumerateCompoundLibrary.py  [--colmode <collabel or colnum>] [--colRxnName <text or number>]
 696                                       [--colRxnSMARTS <text or number>] [--compute2DCoords <yes or no>] [--infileParams <Name,Value,...>]
 697                                       [--mode <RxnByName or RxnBySMARTS>] [--outfileParams <Name,Value,...>] [--overwrite]
 698                                       [--prodMolNames <UseReactants or Sequential>] [--rxnName <text>]
 699                                       [--rxnNamesFile <FileName or auto>] [--smartsRxn <text>] [--sanitize <yes or no>]
 700                                       [-w <dir>] -i  <ReactantFile1,...> -o <outfile>
 701     RDKitEnumerateCompoundLibrary.py [--colmode <collabel or colnum>] [--colRxnName <text or number>] [--colRxnSMARTS <text or number>]
 702                                       [--rxnNamesFile <FileName or auto>] -l | --list
 703     RDKitEnumerateCompoundLibrary.py -h | --help | -e | --examples
 704 
 705 Description:
 706     Perform a combinatorial enumeration of a virtual library of molecules for a reaction specified
 707     using a reaction name or SMARTS pattern and reactant input files.
 708 
 709     The SMARTS patterns for supported reactions names [ Ref 134 ] are retrieved from file,
 710     ReactionNamesAndSMARTS.csv, available in MayaChemTools data directory. The current
 711     list of supported reaction names is shown below:
 712 
 713     '1,2,4_triazole_acetohydrazide', '1,2,4_triazole_carboxylic_acid_ester', 3_nitrile_pyridine,
 714     Benzimidazole_derivatives_aldehyde, Benzimidazole_derivatives_carboxylic_acid_ester,
 715     Benzofuran, Benzothiazole, Benzothiophene, Benzoxazole_aromatic_aldehyde,
 716     Benzoxazole_carboxylic_acid, Buchwald_Hartwig, Decarboxylative_coupling, Fischer_indole,
 717     Friedlaender_chinoline, Grignard_alcohol, Grignard_carbonyl, Heck_non_terminal_vinyl,
 718     Heck_terminal_vinyl, Heteroaromatic_nuc_sub, Huisgen_Cu_catalyzed_1,4_subst,
 719     Huisgen_disubst_alkyne, Huisgen_Ru_catalyzed_1,5_subst, Imidazole, Indole, Mitsunobu_imide,
 720     Mitsunobu_phenole, Mitsunobu_sulfonamide, Mitsunobu_tetrazole_1, Mitsunobu_tetrazole_2,
 721     Mitsunobu_tetrazole_3, Mitsunobu_tetrazole_4, N_arylation_heterocycles, Negishi,
 722     Niementowski_quinazoline, Nucl_sub_aromatic_ortho_nitro, Nucl_sub_aromatic_para_nitro,
 723     Oxadiazole, Paal_Knorr_pyrrole, Phthalazinone, Pictet_Spengler, Piperidine_indole,
 724     Pyrazole, Reductive_amination, Schotten_Baumann_amide, Sonogashira, Spiro_chromanone,
 725     Stille, Sulfon_amide, Suzuki, Tetrazole_connect_regioisomer_1, Tetrazole_connect_regioisomer_2,
 726     Tetrazole_terminal, Thiazole, Thiourea, Triaryl_imidazole, Urea, Williamson_ether, Wittig 
 727 
 728     The supported input file formats are: SD (.sdf, .sd), SMILES (.smi, .csv, .tsv, .txt)
 729 
 730     The supported output file formats are:  SD (.sdf, .sd), SMILES (.smi)
 731 
 732 Options:
 733     -c, --colmode <collabel or colnum>  [default: collabel]
 734         Use column number or name for the specification of columns in a CSV
 735         file containing reaction names along with reaction SMARTS. You may
 736         specify a reaction names file using '--rxnNamesFile' option.
 737     --colRxnName <text or number>  [default: auto]
 738         Column name or number corresponding to reaction names. The default value
 739         is automatically set based on the value of '-c, --colmode': 'RxnName'  for
 740         'collabel'; Reaction name column number for 'colnum'.
 741     --colRxnSMARTS <text or number>  [default: auto]
 742         Column name or number corresponding to reaction SMARTS strings. The default
 743         value is automatically set based on the value of '-c, --colmode': 'RxnSMARTS'
 744         for 'collabel'; Reacton SMARTS column number for 'colnum'.
 745     --compute2DCoords <yes or no>  [default: yes]
 746         Compute 2D coordinates of product molecules before writing them out.
 747     -i, --infiles <ReactantFile1, ReactantFile2...>
 748         Comma delimited list of reactant file names for enumerating a compound library
 749         using reaction SMARTS. The number of reactant files must match number of
 750         reaction components in reaction SMARTS. All reactant input files must have
 751         the same format.
 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. These parameters apply
 762         to all reactant input files, which must have the same file format.
 763     -e, --examples
 764         Print examples.
 765     -h, --help
 766         Print this help message.
 767     -l, --list
 768         List available reaction names along with corresponding SMARTS patterns without
 769         performing any enumeration. In addition, reaction SMARTS patterns are validated.
 770     -m, --mode <RxnByName or RxnBySMARTS>  [default: RxnByName]
 771         Indicate whether a reaction is specified by a reaction name or a SMARTS pattern.
 772         Possible values: RxnByName or RxnBySMARTS.
 773     -o, --outfile <outfile>
 774         Output file name.
 775     --outfileParams <Name,Value,...>  [default: auto]
 776         A comma delimited list of parameter name and value pairs for writing
 777         molecules to files. The supported parameter names for different file
 778         formats, along with their default values, are shown below:
 779             
 780             SD: kekulize,yes,forceV3000,no
 781             SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes,
 782                 smilesTitleLine,yes
 783             
 784     -p, --prodMolNames <UseReactants or Sequential>  [default: UseReactants]
 785         Generate names of product molecules using reactant names or assign names in
 786         a sequential order. Possible values: UseReactants or Sequential. Format of
 787         molecule names: UseReactants - <ReactName1>_<ReactName2>..._Prod<Num>;
 788         Sequential - Prod<Num>
 789     --overwrite
 790         Overwrite existing files.
 791     -r, --rxnName <text>
 792         Name of a reaction to use for enumerating a compound library. This option
 793         is only used during 'RxnByName' value of '-m, --mode' option.
 794     --rxnNamesFile <FileName or auto>  [default: auto]
 795         Specify a file name containing data for names of reactions and SMARTS patterns or
 796         use default file, ReactionNamesAndSMARTS.csv, available in MayaChemTools data
 797         directory.
 798         
 799         Default reactions SMARTS file format: RxnName,RxnSMARTS.
 800         
 801         The local file format is assumed to be same as the default file format. You may
 802         explicitly specify column names or numbers for reaction name and reaction
 803         SMARTS using '--colRxnName' and '--colRxnSMARTS' options.
 804     -s, --smartsRxn <text>
 805         SMARTS pattern of a reaction to use for enumerating a compound library. This
 806         option is only used during 'RxnBySMARTS' value of '-m, --mode' option.
 807     --sanitize <yes or no>  [default: yes]
 808         Sanitize product molecules before writing them out.
 809     -w, --workingdir <dir>
 810         Location of working directory which defaults to the current directory.
 811 
 812 Examples:
 813     To list all available reaction names along with their SMARTS pattern, type:
 814 
 815          % RDKitEnumerateCompoundLibrary.py -l
 816 
 817     To perform a combinatorial enumeration of a virtual compound library corresponding
 818     to named amide reaction, Schotten_Baumann_amide, and write out a SMILES file
 819     type:
 820 
 821         % RDKitEnumerateCompoundLibrary.py -r Schotten_Baumann_amide
 822           -i 'SampleAcids.smi,SampleAmines.smi' -o SampleOutCmpdLibrary.smi
 823 
 824     To run the previous command using a local reaction names file with explicit
 825     specification of column names containing reaction names and SMARTS, and write
 826      out a SMILES file type:
 827 
 828         % RDKitEnumerateCompoundLibrary.py -r Schotten_Baumann_amide
 829           --rxnNamesFile ReactionNamesAndSMARTS.csv
 830           --colmode collabel --colRxnName RxnName --colRxnSMARTS RxnSMARTS
 831           -i 'SampleAcids.smi,SampleAmines.smi' -o SampleOutCmpdLibrary.smi
 832 
 833     To perform a combinatorial enumeration of a virtual compound library corresponding
 834     to an amide reaction specified using a SMARTS pattern and write out a SD file containing
 835     sanitized molecules, computed 2D coordinates, and generation of molecule names from
 836     reactant names, type:
 837 
 838         % RDKitEnumerateCompoundLibrary.py -m RxnBySMARTS
 839           -s '[O:2]=[C:1][OH].[N:3]>>[O:2]=[C:1][N:3]'
 840           -i 'SampleAcids.smi,SampleAmines.smi' -o SampleOutCmpdLibrary.sdf
 841 
 842     To perform a combinatorial enumeration of a virtual compound library corresponding
 843     to an amide reaction specified using a SMARTS pattern  and write out a SD file containing
 844     unsanitized molecules, without generating 2D coordinates, and a sequential generation
 845     of molecule names, type:
 846 
 847         % RDKitEnumerateCompoundLibrary.py -m RxnBySMARTS -c no --sanitize no
 848           -p Sequential -s '[O:2]=[C:1][OH].[N:3]>>[O:2]=[C:1][N:3]'
 849           -i 'SampleAcids.smi,SampleAmines.smi' -o SampleOutCmpdLibrary.sdf
 850 
 851 Author:
 852     Manish Sud(msud@san.rr.com)
 853 
 854 See also:
 855     RDKitConvertFileFormat.py, RDKitFilterPAINS.py, RDKitSearchFunctionalGroups.py,
 856     RDKitSearchSMARTS.py
 857 
 858 Copyright:
 859     Copyright (C) 2026 Manish Sud. All rights reserved.
 860 
 861     The functionality available in this script is implemented using RDKit, an
 862     open source toolkit for cheminformatics developed by Greg Landrum.
 863 
 864     This file is part of MayaChemTools.
 865 
 866     MayaChemTools is free software; you can redistribute it and/or modify it under
 867     the terms of the GNU Lesser General Public License as published by the Free
 868     Software Foundation; either version 3 of the License, or (at your option) any
 869     later version.
 870 
 871 """
 872 
 873 if __name__ == "__main__":
 874     main()