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