MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: RDKitEnumerateTautomers.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 import multiprocessing as mp
  36 
  37 # RDKit imports...
  38 try:
  39     from rdkit import rdBase
  40     from rdkit import Chem
  41     from rdkit.Chem.MolStandardize import rdMolStandardize
  42     from rdkit.Chem import AllChem
  43 except ImportError as ErrMsg:
  44     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
  45     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
  46     sys.exit(1)
  47 
  48 # MayaChemTools imports...
  49 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  50 try:
  51     from docopt import docopt
  52     import MiscUtil
  53     import RDKitUtil
  54 except ImportError as ErrMsg:
  55     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  56     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  57     sys.exit(1)
  58 
  59 ScriptName = os.path.basename(sys.argv[0])
  60 Options = {}
  61 OptionsInfo = {}
  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     EnumerateTautomers()
  82 
  83     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  84     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  85 
  86 
  87 def EnumerateTautomers():
  88     """Enunmerate tautomers."""
  89 
  90     # Setup a molecule reader...
  91     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
  92     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
  93 
  94     # Set up a molecule writer...
  95     Writer = SetupMoleculeWriter()
  96 
  97     MolCount, ValidMolCount, TautomerizationFailedCount, TautomersCount, MinTautomersCount, MaxTautomersCount = (
  98         ProcessMolecules(Mols, Writer)
  99     )
 100 
 101     if Writer is not None:
 102         Writer.close()
 103 
 104     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
 105     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
 106     MiscUtil.PrintInfo("Number of molecules failed during tautomerization: %d" % TautomerizationFailedCount)
 107     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + TautomerizationFailedCount))
 108 
 109     MiscUtil.PrintInfo("\nNumber of tautomerized molecules: %d" % (ValidMolCount - TautomerizationFailedCount))
 110 
 111     MiscUtil.PrintInfo("\nTotal number of tautomers for molecules: %d" % TautomersCount)
 112     MiscUtil.PrintInfo("Minumum number of tautomers for a molecule: %d" % MinTautomersCount)
 113     MiscUtil.PrintInfo("Maxiumum number of tautomers for a molecule: %d" % MaxTautomersCount)
 114     MiscUtil.PrintInfo(
 115         "Average number of tautomers for a molecule: %.1f"
 116         % (TautomersCount / (ValidMolCount - TautomerizationFailedCount))
 117     )
 118 
 119 
 120 def ProcessMolecules(Mols, Writer):
 121     """Process molecules."""
 122 
 123     if OptionsInfo["MPMode"]:
 124         return ProcessMoleculesUsingMultipleProcesses(Mols, Writer)
 125     else:
 126         return ProcessMoleculesUsingSingleProcess(Mols, Writer)
 127 
 128 
 129 def ProcessMoleculesUsingSingleProcess(Mols, Writer):
 130     """Process and generate tautomers for molecules using a single process."""
 131 
 132     MiscUtil.PrintInfo("\nEnumerating tatutomers...")
 133 
 134     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 135     SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"]
 136 
 137     # Set up tautomer enumerator...
 138     TautomerEnumerator = SetupTautomerEnumerator()
 139 
 140     (MolCount, ValidMolCount, TautomerizationFailedCount, TautomersCount) = [0] * 4
 141     (MinTautomersCount, MaxTautomersCount) = [sys.maxsize, 0]
 142     FirstTautomerMol = True
 143     for Mol in Mols:
 144         MolCount += 1
 145 
 146         if Mol is None:
 147             continue
 148 
 149         if RDKitUtil.IsMolEmpty(Mol):
 150             if not OptionsInfo["QuietMode"]:
 151                 MolName = RDKitUtil.GetMolName(Mol, MolCount)
 152                 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 153             continue
 154 
 155         ValidMolCount += 1
 156 
 157         TautomerMols, TautomerizationStatus = EnumerateMolTautomers(Mol, TautomerEnumerator, MolCount)
 158         if not TautomerizationStatus:
 159             if not OptionsInfo["QuietMode"]:
 160                 MolName = RDKitUtil.GetMolName(Mol, MolCount)
 161                 MiscUtil.PrintWarning("Failed to tautomerize molecule %s" % MolName)
 162 
 163             TautomerizationFailedCount += 1
 164             continue
 165 
 166         if FirstTautomerMol:
 167             FirstTautomerMol = False
 168             if SetSMILESMolProps:
 169                 RDKitUtil.SetWriterMolProps(Writer, TautomerMols[0])
 170 
 171         # Track tautomer count...
 172         TautomerMolsCount = len(TautomerMols)
 173         TautomersCount += TautomerMolsCount
 174         if TautomerMolsCount < MinTautomersCount:
 175             MinTautomersCount = TautomerMolsCount
 176         if TautomerMolsCount > MaxTautomersCount:
 177             MaxTautomersCount = TautomerMolsCount
 178 
 179         WriteMolTautomers(Writer, Mol, MolCount, Compute2DCoords, TautomerMols)
 180 
 181     return (MolCount, ValidMolCount, TautomerizationFailedCount, TautomersCount, MinTautomersCount, MaxTautomersCount)
 182 
 183 
 184 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer):
 185     """Process and enumerate tautomer of molecules using  multiprocessing."""
 186 
 187     MiscUtil.PrintInfo("\nEnumerating tatutomers using multiprocessing...")
 188 
 189     MPParams = OptionsInfo["MPParams"]
 190     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 191 
 192     # Setup data for initializing a worker process...
 193     InitializeWorkerProcessArgs = (
 194         MiscUtil.ObjectToBase64EncodedString(Options),
 195         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
 196     )
 197 
 198     # Setup a encoded mols data iterable for a worker process by pickling only public
 199     # and private molecule properties...
 200     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
 201 
 202     # Setup process pool along with data initialization for each process...
 203     MiscUtil.PrintInfo(
 204         "\nConfiguring multiprocessing using %s method..."
 205         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
 206     )
 207     MiscUtil.PrintInfo(
 208         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
 209         % (
 210             MPParams["NumProcesses"],
 211             MPParams["InputDataMode"],
 212             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
 213         )
 214     )
 215 
 216     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
 217 
 218     # Start processing...
 219     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
 220         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
 221     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
 222         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
 223     else:
 224         MiscUtil.PrintError(
 225             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
 226         )
 227 
 228     SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"]
 229 
 230     (MolCount, ValidMolCount, TautomerizationFailedCount, TautomersCount) = [0] * 4
 231     (MinTautomersCount, MaxTautomersCount) = [sys.maxsize, 0]
 232     FirstTautomerMol = True
 233     for Result in Results:
 234         MolCount += 1
 235         MolIndex, EncodedMol, TautomerizationStatus, EncodedTautomerMols = Result
 236 
 237         if EncodedMol is None:
 238             continue
 239         ValidMolCount += 1
 240 
 241         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
 242 
 243         TautomerMols = []
 244         if EncodedTautomerMols is not None:
 245             TautomerMols = [
 246                 RDKitUtil.MolFromBase64EncodedMolString(EncodedTautomerMol)
 247                 for EncodedTautomerMol in EncodedTautomerMols
 248             ]
 249 
 250         if not TautomerizationStatus:
 251             if not OptionsInfo["QuietMode"]:
 252                 MolName = RDKitUtil.GetMolName(Mol, MolCount)
 253                 MiscUtil.PrintWarning("Failed to tautomerize molecule %s" % MolName)
 254 
 255             TautomerizationFailedCount += 1
 256             continue
 257 
 258         if FirstTautomerMol:
 259             FirstTautomerMol = False
 260             if SetSMILESMolProps:
 261                 RDKitUtil.SetWriterMolProps(Writer, TautomerMols[0])
 262 
 263         # Track tautomer count...
 264         TautomerMolsCount = len(TautomerMols)
 265         TautomersCount += TautomerMolsCount
 266         if TautomerMolsCount < MinTautomersCount:
 267             MinTautomersCount = TautomerMolsCount
 268         if TautomerMolsCount > MaxTautomersCount:
 269             MaxTautomersCount = TautomerMolsCount
 270 
 271         WriteMolTautomers(Writer, Mol, MolCount, Compute2DCoords, TautomerMols)
 272 
 273     return (MolCount, ValidMolCount, TautomerizationFailedCount, TautomersCount, MinTautomersCount, MaxTautomersCount)
 274 
 275 
 276 def InitializeWorkerProcess(*EncodedArgs):
 277     """Initialize data for a worker process."""
 278 
 279     global Options, OptionsInfo
 280 
 281     MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
 282 
 283     # Decode Options and OptionInfo...
 284     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
 285     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
 286 
 287     # Set up tautomer enumerator...
 288     OptionsInfo["TautomerEnumerator"] = SetupTautomerEnumerator()
 289 
 290 
 291 def WorkerProcess(EncodedMolInfo):
 292     """Process data for a worker process."""
 293 
 294     MolIndex, EncodedMol = EncodedMolInfo
 295 
 296     if EncodedMol is None:
 297         return [MolIndex, None, False, None]
 298 
 299     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
 300     if RDKitUtil.IsMolEmpty(Mol):
 301         if not OptionsInfo["QuietMode"]:
 302             MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
 303             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 304         return [MolIndex, None, False, None]
 305 
 306     TautomerMols, TautomerizationStatus = EnumerateMolTautomers(Mol, OptionsInfo["TautomerEnumerator"], (MolIndex + 1))
 307 
 308     EncodedTautomerMols = None
 309     if TautomerMols is not None:
 310         EncodedTautomerMols = [
 311             RDKitUtil.MolToBase64EncodedMolString(
 312                 TautomerMol,
 313                 PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps
 314                 | Chem.PropertyPickleOptions.AtomProps
 315                 | Chem.PropertyPickleOptions.BondProps
 316                 | Chem.PropertyPickleOptions.PrivateProps,
 317             )
 318             for TautomerMol in TautomerMols
 319         ]
 320 
 321     return [MolIndex, EncodedMol, TautomerizationStatus, EncodedTautomerMols]
 322 
 323 
 324 def EnumerateMolTautomers(Mol, TautomerEnumerator, MolNum):
 325     """Enumerate tautomers of a molecule and return a list of tatutomers
 326     along with the status of tautomerization."""
 327 
 328     TautomerMols, Status, TautomerScores = [None, False, None]
 329     try:
 330         TautomerMols = [TautomerMol for TautomerMol in TautomerEnumerator.Enumerate(Mol)]
 331 
 332         if OptionsInfo["ScoreTautomers"]:
 333             TautomerScores = [TautomerEnumerator.ScoreTautomer(TautomerMol) for TautomerMol in TautomerMols]
 334 
 335         if OptionsInfo["SortTautomers"]:
 336             TautomerMols, TautomerScores = SortMolTautomers(Mol, TautomerEnumerator, TautomerMols, TautomerScores)
 337 
 338         # Set tautomer score...
 339         if TautomerScores is not None:
 340             for Index, TautomerMol in enumerate(TautomerMols):
 341                 TautomerMol.SetProp("Tautomer_Score", "%.1f" % TautomerScores[Index])
 342 
 343         Status = True
 344     except Exception as ErrMsg:
 345         if not OptionsInfo["QuietMode"]:
 346             MiscUtil.PrintWarning("Failed to tautomerize molecule %s: %s" % (RDKitUtil.GetMolName(Mol, MolNum), ErrMsg))
 347         TautomerMols, Status = [None, False]
 348 
 349     return (TautomerMols, Status)
 350 
 351 
 352 def SortMolTautomers(Mol, TautomerEnumerator, TautomerMols, TautomerScores=None):
 353     """Sort tatutomers by SMILES string and place canonical tautomer at the top
 354     of the list."""
 355 
 356     CanonicalTautomer = TautomerEnumerator.Canonicalize(Mol)
 357     CanonicalTautomerSmiles = Chem.MolToSmiles(CanonicalTautomer)
 358     if TautomerScores is None:
 359         CanonicalTautomerScore = None
 360     else:
 361         CanonicalTautomerScore = TautomerEnumerator.ScoreTautomer(CanonicalTautomer)
 362 
 363     TautomerSmiles = [Chem.MolToSmiles(TautomerMol) for TautomerMol in TautomerMols]
 364     if TautomerScores is None:
 365         SortedResults = sorted(
 366             (Smiles, TautomerMol)
 367             for Smiles, TautomerMol in zip(TautomerSmiles, TautomerMols)
 368             if Smiles != CanonicalTautomerSmiles
 369         )
 370     else:
 371         SortedResults = sorted(
 372             (Smiles, TautomerMol, TautomerScore)
 373             for Smiles, TautomerMol, TautomerScore in zip(TautomerSmiles, TautomerMols, TautomerScores)
 374             if Smiles != CanonicalTautomerSmiles
 375         )
 376 
 377     SortedTautomerMols = [CanonicalTautomer]
 378     if TautomerScores is None:
 379         SortedTautomerMols += [TautomerMol for Smiles, TautomerMol in SortedResults]
 380     else:
 381         SortedTautomerMols += [TautomerMol for Smiles, TautomerMol, TautomerScore in SortedResults]
 382 
 383     if TautomerScores is None:
 384         SortedTautomerScores = None
 385     else:
 386         SortedTautomerScores = [CanonicalTautomerScore]
 387         SortedTautomerScores += [TautomerScore for Smiles, TautomerMol, TautomerScore in SortedResults]
 388 
 389     return (SortedTautomerMols, SortedTautomerScores)
 390 
 391 
 392 def WriteMolTautomers(Writer, Mol, MolNum, Compute2DCoords, TautomerMols):
 393     """Write out tautomers of a  molecule."""
 394 
 395     if TautomerMols is None:
 396         return
 397 
 398     MolName = RDKitUtil.GetMolName(Mol, MolNum)
 399 
 400     for Index, TautomerMol in enumerate(TautomerMols):
 401         SetupTautomerMolName(TautomerMol, MolName, (Index + 1))
 402 
 403         if Compute2DCoords:
 404             AllChem.Compute2DCoords(Mol)
 405 
 406         Writer.write(TautomerMol)
 407 
 408 
 409 def SetupTautomerMolName(Mol, MolName, TautomerCount):
 410     """Set tautomer mol name."""
 411 
 412     TautomerName = "%s_Taut%d" % (MolName, TautomerCount)
 413     Mol.SetProp("_Name", TautomerName)
 414 
 415 
 416 def SetupTautomerEnumerator():
 417     """Setup tautomer enumerator."""
 418 
 419     TautomerParams = SetupTautomerizationParameters()
 420 
 421     return rdMolStandardize.TautomerEnumerator(TautomerParams)
 422 
 423 
 424 def SetupTautomerizationParameters():
 425     """Setup tautomerization parameters for RDKit using cleanup parameters."""
 426 
 427     Params = rdMolStandardize.CleanupParameters()
 428     TautomerizationParams = OptionsInfo["TautomerizationParams"]
 429 
 430     if TautomerizationParams["TautomerTransformsFile"] is not None:
 431         Params.tautomerTransformsFile = TautomerizationParams["TautomerTransformsFile"]
 432 
 433     Params.maxTautomers = TautomerizationParams["MaxTautomers"]
 434     Params.maxTransforms = TautomerizationParams["MaxTransforms"]
 435     Params.tautomerRemoveBondStereo = TautomerizationParams["TautomerRemoveBondStereo"]
 436     Params.tautomerRemoveIsotopicHs = TautomerizationParams["TautomerRemoveIsotopicHs"]
 437     Params.tautomerRemoveSp3Stereo = TautomerizationParams["TautomerRemoveSp3Stereo"]
 438     Params.tautomerReassignStereo = TautomerizationParams["TautomerReassignStereo"]
 439 
 440     return Params
 441 
 442 
 443 def SetupMoleculeWriter():
 444     """Setup a molecule writer."""
 445 
 446     Writer = None
 447 
 448     Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
 449     if Writer is None:
 450         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
 451     MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"])
 452 
 453     return Writer
 454 
 455 
 456 def ProcessTautomerizationParameters():
 457     """Process tautomerizationparameters."""
 458 
 459     ParamsDefaultInfo = {
 460         "TautomerTransformsFile": ["file", None],
 461         "MaxTautomers": ["int", 1000],
 462         "MaxTransforms": ["int", 1000],
 463         "TautomerRemoveBondStereo": ["bool", True],
 464         "TautomerRemoveIsotopicHs": ["bool", True],
 465         "TautomerRemoveSp3Stereo": ["bool", True],
 466         "TautomerReassignStereo": ["bool", True],
 467     }
 468 
 469     OptionsInfo["TautomerizationParams"] = MiscUtil.ProcessOptionNameValuePairParameters(
 470         "--tautomerizationParams", Options["--tautomerizationParams"], ParamsDefaultInfo
 471     )
 472 
 473     #  Validate numerical values...
 474     for ParamName in ["MaxTautomers", "MaxTransforms"]:
 475         ParamValue = OptionsInfo["TautomerizationParams"][ParamName]
 476         if ParamValue <= 0:
 477             MiscUtil.PrintError(
 478                 'The parameter value, %s, specified for parameter name, %s, using "-t, --tautomerizationParams" option is not a valid value. Supported values: > 0'
 479                 % (ParamValue, ParamName)
 480             )
 481 
 482 
 483 def ProcessOptions():
 484     """Process and validate command line arguments and options."""
 485 
 486     MiscUtil.PrintInfo("Processing options...")
 487 
 488     # Validate options...
 489     ValidateOptions()
 490 
 491     OptionsInfo["Infile"] = Options["--infile"]
 492     ParamsDefaultInfoOverride = {"RemoveHydrogens": False}
 493     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 494         "--infileParams", Options["--infileParams"], Options["--infile"], ParamsDefaultInfo=ParamsDefaultInfoOverride
 495     )
 496 
 497     OptionsInfo["Outfile"] = Options["--outfile"]
 498     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 499         "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"]
 500     )
 501 
 502     OptionsInfo["Overwrite"] = Options["--overwrite"]
 503 
 504     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 505     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 506 
 507     OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False
 508 
 509     OptionsInfo["ScoreTautomers"] = True if re.match("^yes$", Options["--scoreTautomers"], re.I) else False
 510     OptionsInfo["SortTautomers"] = True if re.match("^yes$", Options["--sortTautomers"], re.I) else False
 511 
 512     ProcessTautomerizationParameters()
 513 
 514 
 515 def RetrieveOptions():
 516     """Retrieve command line arguments and options."""
 517 
 518     # Get options...
 519     global Options
 520     Options = docopt(_docoptUsage_)
 521 
 522     # Set current working directory to the specified directory...
 523     WorkingDir = Options["--workingdir"]
 524     if WorkingDir:
 525         os.chdir(WorkingDir)
 526 
 527     # Handle examples option...
 528     if "--examples" in Options and Options["--examples"]:
 529         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 530         sys.exit(0)
 531 
 532 
 533 def ValidateOptions():
 534     """Validate option values."""
 535 
 536     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 537     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv")
 538 
 539     if Options["--outfile"]:
 540         MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi")
 541         MiscUtil.ValidateOptionsOutputFileOverwrite(
 542             "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 543         )
 544         MiscUtil.ValidateOptionsDistinctFileNames(
 545             "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 546         )
 547 
 548     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 549     MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no")
 550 
 551     MiscUtil.ValidateOptionTextValue("--scoreTautomers", Options["--scoreTautomers"], "yes no")
 552     MiscUtil.ValidateOptionTextValue("--sortTautomers", Options["--sortTautomers"], "yes no")
 553 
 554 
 555 # Setup a usage string for docopt...
 556 _docoptUsage_ = """
 557 RDKitEnumerateTautomers.py - Enumerate tautomers of molecules
 558 
 559 Usage:
 560     RDKitEnumerateTautomers.py [--infileParams <Name,Value,...>] [--mp <yes or no>] [--mpParams <Name,Value,...>]
 561                                [--outfileParams <Name,Value,...> ] [--overwrite] [--quiet <yes or no>] [--scoreTautomers <yes or no>]
 562                                [--sortTautomers <yes or no>] [--tautomerizationParams <Name,Value,...>] [-w <dir>] -i <infile> -o <outfile>
 563     RDKitEnumerateTautomers.py -h | --help | -e | --examples
 564 
 565 Description:
 566     Enumerate tautomers for molecules and write them out to an output file.
 567     The tautomer enumerator generates both protomers and valence tautomers. You
 568     may optionally calculate tautomer scores and sort tautomers by SMILES string. The
 569     canonical tautomer is placed at the top during sorting.
 570 
 571     The supported input file formats are: SD (.sdf, .sd), SMILES (.smi., csv, .tsv, .txt)
 572 
 573     The supported output file formats are: SD (.sdf, .sd), SMILES (.smi)
 574 
 575 Options:
 576     -e, --examples
 577         Print examples.
 578     -h, --help
 579         Print this help message.
 580     -i, --infile <infile>
 581         Input file name.
 582     --infileParams <Name,Value,...>  [default: auto]
 583         A comma delimited list of parameter name and value pairs for reading
 584         molecules from files. The supported parameter names for different file
 585         formats, along with their default values, are shown below:
 586             
 587             SD, MOL: removeHydrogens,no,sanitize,yes,strictParsing,yes
 588             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 589                 smilesTitleLine,auto,sanitize,yes
 590             
 591         Possible values for smilesDelimiter: space, comma or tab.
 592     --mp <yes or no>  [default: no]
 593         Use multiprocessing.
 594          
 595         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 596         function employing lazy RDKit data iterable. This allows processing of
 597         arbitrary large data sets without any additional requirements memory.
 598         
 599         All input data may be optionally loaded into memory by mp.Pool.map()
 600         before starting worker processes in a process pool by setting the value
 601         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 602         
 603         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 604         data mode may adversely impact the performance. The '--mpParams' section
 605         provides additional information to tune the value of 'chunkSize'.
 606     --mpParams <Name,Value,...>  [default: auto]
 607         A comma delimited list of parameter name and value pairs to configure
 608         multiprocessing.
 609         
 610         The supported parameter names along with their default and possible
 611         values are shown below:
 612         
 613             chunkSize, auto
 614             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 615             numProcesses, auto   [ Default: mp.cpu_count() ]
 616         
 617         These parameters are used by the following functions to configure and
 618         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 619         mp.Pool.imap().
 620         
 621         The chunkSize determines chunks of input data passed to each worker
 622         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 623         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 624         
 625         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 626         automatically converts RDKit data iterable into a list, loads all data into
 627         memory, and calculates the default chunkSize using the following method
 628         as shown in its code:
 629         
 630             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 631             if extra: chunkSize += 1
 632         
 633         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 634         and 100 data items.
 635         
 636         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 637         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 638         data into memory. Consequently, the size of input data is not known a priori.
 639         It's not possible to estimate an optimal value for the chunkSize. The default 
 640         chunkSize is set to 1.
 641         
 642         The default value for the chunkSize during 'Lazy' data mode may adversely
 643         impact the performance due to the overhead associated with exchanging
 644         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 645         a larger value during 'Lazy' input data mode, based on the size of your input
 646         data and number of processes in the process pool.
 647         
 648         The mp.Pool.map() function waits for all worker processes to process all
 649         the data and return the results. The mp.Pool.imap() function, however,
 650         returns the the results obtained from worker processes as soon as the
 651         results become available for specified chunks of data.
 652         
 653         The order of data in the results returned by both mp.Pool.map() and 
 654         mp.Pool.imap() functions always corresponds to the input data.
 655     -o, --outfile <outfile>
 656         Output file name.
 657     --outfileParams <Name,Value,...>  [default: auto]
 658         A comma delimited list of parameter name and value pairs for writing
 659         molecules to files. The supported parameter names for different file
 660         formats, along with their default values, are shown below:
 661             
 662             SD: compute2DCoords,auto,kekulize,yes,forceV3000,no
 663             SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes,
 664                 smilesTitleLine,yes,smilesMolName,yes,smilesMolProps,no
 665             
 666         Default value for compute2DCoords: yes for SMILES input file; no for all other
 667         file types.
 668     --overwrite
 669         Overwrite existing files.
 670     -q, --quiet <yes or no>  [default: no]
 671         Use quiet mode. The warning and information messages will not be printed.
 672     --scoreTautomers <yes or no>  [default: no]
 673         Calculate and write out tautomer scores [ Ref 159 ].
 674     --sortTautomers <yes or no>  [default: no]
 675         Sort tatutomers of a molecule by SMILES string and place canonical tautomer
 676         at the top of the list.
 677     -t, --tautomerizationParams <Name,Value,...>  [default: auto]
 678         A comma delimited list of parameter name and value pairs for enumerating
 679         tautomers of molecules. The supported parameter names along with their
 680         default values are shown below:
 681             
 682             tautomerTransformsFile,none,
 683             maxTautomers,1000,maxTransforms,1000,
 684             tautomerRemoveBondStereo,yes,tautomerRemoveIsotopicHs,yes
 685             tautomerRemoveSp3Stereo,yes,tautomerReassignStereo,yes
 686             
 687         A brief description of the tatutomerization parameters, taken from RDKit
 688         documentation, is as follows:
 689             
 690             tautomerTransformsFile - File containing tautomer transformations
 691             
 692             maxTautomers - Maximum number of tautomers to generate
 693             maxTransforms - Maximum number of transforms to apply during
 694                 tautomer enumeration
 695             tautomerRemoveBondStereo - Remove stereochemistry from double bonds
 696                 involved in tautomerism
 697             tautomerRemoveIsotopicHs: Remove isotopic Hs from centers involved in tautomerism
 698             tautomerRemoveSp3Stereo - Remove stereochemistry from sp3 centers
 699                 involved in tautomerism
 700             tautomerReassignStereo - AssignStereochemistry on all generated tautomers
 701             
 702         The default value is set to none for the 'tautomerTransformsFile' parameter. The
 703         script relies on RDKit to automatically load appropriate tautomer transformations
 704         from a set of internal catalog.
 705         
 706         The contents  of transformation file are described below:
 707             
 708             tautomerTransformsFile - File containing tautomer transformations
 709             
 710                 // Name                SMARTS   Bonds  Charges
 711                 1,3 (thio)keto/enol f  [CX4!H0]-[C]=[O,S,Se,Te;X1]
 712                 1,3 (thio)keto/enol r  [O,S,Se,Te;X2!H0]-[C]=[C]
 713                 1,5 (thio)keto/enol f  [CX4,NX3;!H0]-[C]=[C][CH0]=[O,S,Se,Te;X1]
 714                 ... ... ...
 715             
 716     -w, --workingdir <dir>
 717         Location of working directory which defaults to the current directory.
 718 
 719 Examples:
 720     To enumerate tautomers of molecules in a SMILES file and write out a SMILES
 721     file, type: 
 722 
 723         % RDKitEnumerateTautomers.py -i Sample.smi -o SampleOut.smi
 724 
 725     To enumerate tautomers of molecules in a SD file, calculate tautomer scores,
 726     sort tautomers, and write out a SD file, type:
 727 
 728         % RDKitEnumerateTautomers.py --scoreTautomers yes --sortTautomers yes
 729           -i Sample.sdf -o SampleOut.sdf
 730 
 731     To enumerate tautomers of molecules in a SD fie , calculate tautomer
 732     scores, sort tautomers, and write out a SMILES file, type:
 733 
 734         % RDKitEnumerateTautomers.py --scoreTautomers yes  --sortTautomers yes
 735           --outfileParams "smilesMolProps,yes" -i Sample.smi -o SampleOut.smi
 736 
 737     To enumerate tautomers of  molecules in a SD file, performing enumeration in
 738     multiprocessing mode on all available CPUs without loading all data into
 739     memory, and write out a SD file, type:
 740 
 741         % RDKitEnumerateTautomers.py --mp yes -i Sample.sdf -o SampleOut.sdf
 742 
 743     To enumerate tautomers of  molecules in a SD file, performing enumeration in
 744     multiprocessing mode on specific number of CPUs and chunk size without loading
 745     all data into memory, and write out a SD file, type:
 746 
 747         % RDKitEnumerateTautomers.py --mp yes --mpParams "inputDataMode,Lazy,
 748           numProcesses,4,chunkSize,8" -i Sample.sdf -o SampleOut.sdf
 749 
 750     To enumerate tautomers of  molecules in a SD file using specific values of
 751     parameters to contol the enumeration behavior, and write out a SD file, type:
 752 
 753         % RDKitEnumerateTautomers.py  -t "maxTautomers,1000,maxTransforms,1000,
 754           tautomerRemoveBondStereo,yes,tautomerRemoveIsotopicHs,yes,
 755           tautomerRemoveSp3Stereo,yes,tautomerReassignStereo,yes"
 756           --scoreTautomers yes --sortTautomers yes -i Sample.sdf -o SampleOut.sdf
 757 
 758     To enumerate tautomers for molecules in a CSV SMILES file, SMILES strings in column 1,
 759     name in column 2, and generate output SD file, type:
 760 
 761         % RDKitEnumerateTautomers.py --infileParams 
 762           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
 763           smilesNameColumn,2" --outfileParams "compute2DCoords,yes"
 764           -i SampleSMILES.csv -o SampleOut.sdf
 765 
 766 Author:
 767     Manish Sud(msud@san.rr.com)
 768 
 769 See also:
 770     RDKitConvertFileFormat.py, RDKitRemoveDuplicateMolecules.py,
 771     RDKitRemoveInvalidMolecules.py, RDKitRemoveSalts.py,
 772     RDKitSearchFunctionalGroups.py, RDKitSearchSMARTS.py,
 773     RDKitStandardizeMolecules.py
 774 
 775 Copyright:
 776     Copyright (C) 2026 Manish Sud. All rights reserved.
 777 
 778     The functionality available in this script is implemented using RDKit, an
 779     open source toolkit for cheminformatics developed by Greg Landrum.
 780 
 781     This file is part of MayaChemTools.
 782 
 783     MayaChemTools is free software; you can redistribute it and/or modify it under
 784     the terms of the GNU Lesser General Public License as published by the Free
 785     Software Foundation; either version 3 of the License, or (at your option) any
 786     later version.
 787 
 788 """
 789 
 790 if __name__ == "__main__":
 791     main()