1 #!/bin/env python 2 # 3 # File: RDKitStandardizeMolecules.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 StandardizeMolecules() 82 83 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName) 84 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime)) 85 86 87 def StandardizeMolecules(): 88 """Stanardize molecules.""" 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, StandardizationFailedCount = ProcessMolecules(Mols, Writer) 98 99 if Writer is not None: 100 Writer.close() 101 102 MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount) 103 MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount) 104 MiscUtil.PrintInfo("Number of molecules failed during standardization: %d" % StandardizationFailedCount) 105 MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + StandardizationFailedCount)) 106 107 MiscUtil.PrintInfo("\nNumber of standardized molecules: %d" % (ValidMolCount - StandardizationFailedCount)) 108 109 110 def ProcessMolecules(Mols, Writer): 111 """Process and standardize molecules.""" 112 113 if OptionsInfo["MPMode"]: 114 return ProcessMoleculesUsingMultipleProcesses(Mols, Writer) 115 else: 116 return ProcessMoleculesUsingSingleProcess(Mols, Writer) 117 118 119 def ProcessMoleculesUsingSingleProcess(Mols, Writer): 120 """Process and standardize molecules using a single process.""" 121 122 MiscUtil.PrintInfo("\nStandardizing molecules...") 123 124 Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"] 125 SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"] 126 127 # Set up standardize... 128 SetupStandardize() 129 130 (MolCount, ValidMolCount, StandardizationFailedCount) = [0] * 3 131 FirstMol = True 132 for Mol in Mols: 133 MolCount += 1 134 135 if Mol is None: 136 continue 137 138 if RDKitUtil.IsMolEmpty(Mol): 139 if not OptionsInfo["QuietMode"]: 140 MolName = RDKitUtil.GetMolName(Mol, MolCount) 141 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 142 continue 143 144 ValidMolCount += 1 145 if FirstMol: 146 FirstMol = False 147 if SetSMILESMolProps: 148 RDKitUtil.SetWriterMolProps(Writer, Mol) 149 150 StandardizedMol, StandardizationStatus = PerformStandardization(Mol, MolCount) 151 if not StandardizationStatus: 152 if not OptionsInfo["QuietMode"]: 153 MolName = RDKitUtil.GetMolName(Mol, MolCount) 154 MiscUtil.PrintWarning("Failed to standardize molecule %s" % MolName) 155 156 StandardizationFailedCount += 1 157 continue 158 159 WriteMolecule(Writer, StandardizedMol, Compute2DCoords) 160 161 return (MolCount, ValidMolCount, StandardizationFailedCount) 162 163 164 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer): 165 """Process and standardize molecules using multiprocessing.""" 166 167 MiscUtil.PrintInfo("\nStandardize molecules using multiprocessing...") 168 169 MPParams = OptionsInfo["MPParams"] 170 Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"] 171 172 # Setup data for initializing a worker process... 173 InitializeWorkerProcessArgs = ( 174 MiscUtil.ObjectToBase64EncodedString(Options), 175 MiscUtil.ObjectToBase64EncodedString(OptionsInfo), 176 ) 177 178 # Setup a encoded mols data iterable for a worker process by pickling only public 179 # and private molecule properties... 180 WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols) 181 182 # Setup process pool along with data initialization for each process... 183 MiscUtil.PrintInfo( 184 "\nConfiguring multiprocessing using %s method..." 185 % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()") 186 ) 187 MiscUtil.PrintInfo( 188 "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n" 189 % ( 190 MPParams["NumProcesses"], 191 MPParams["InputDataMode"], 192 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]), 193 ) 194 ) 195 196 ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs) 197 198 # Start processing... 199 if re.match("^Lazy$", MPParams["InputDataMode"], re.I): 200 Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 201 elif re.match("^InMemory$", MPParams["InputDataMode"], re.I): 202 Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 203 else: 204 MiscUtil.PrintError( 205 'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"]) 206 ) 207 208 SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"] 209 210 (MolCount, ValidMolCount, StandardizationFailedCount) = [0] * 3 211 FirstMol = True 212 for Result in Results: 213 MolCount += 1 214 MolIndex, EncodedMol, EncodedStandardizedMol, StandardizationStatus = Result 215 216 if EncodedMol is None: 217 continue 218 ValidMolCount += 1 219 220 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 221 StandardizedMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedStandardizedMol) 222 223 if FirstMol: 224 FirstMol = False 225 if SetSMILESMolProps: 226 RDKitUtil.SetWriterMolProps(Writer, Mol) 227 228 if not StandardizationStatus: 229 if not OptionsInfo["QuietMode"]: 230 MolName = RDKitUtil.GetMolName(Mol, MolCount) 231 MiscUtil.PrintWarning("Failed to standardize molecule %s" % MolName) 232 233 StandardizationFailedCount += 1 234 continue 235 236 WriteMolecule(Writer, StandardizedMol, Compute2DCoords) 237 238 return (MolCount, ValidMolCount, StandardizationFailedCount) 239 240 241 def InitializeWorkerProcess(*EncodedArgs): 242 """Initialize data for a worker process.""" 243 244 global Options, OptionsInfo 245 246 MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid()) 247 248 # Decode Options and OptionInfo... 249 Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0]) 250 OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1]) 251 252 # Set up standardize... 253 SetupStandardize() 254 255 256 def WorkerProcess(EncodedMolInfo): 257 """Process data for a worker process.""" 258 259 MolIndex, EncodedMol = EncodedMolInfo 260 261 if EncodedMol is None: 262 return [MolIndex, None, None, False] 263 264 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 265 if RDKitUtil.IsMolEmpty(Mol): 266 if not OptionsInfo["QuietMode"]: 267 MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1)) 268 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 269 return [MolIndex, None, None, False] 270 271 StandardizedMol, StandardizationStatus = PerformStandardization(Mol, (MolIndex + 1)) 272 EncodedStandardizedMol = RDKitUtil.MolToBase64EncodedMolString( 273 StandardizedMol, 274 PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps 275 | Chem.PropertyPickleOptions.AtomProps 276 | Chem.PropertyPickleOptions.BondProps 277 | Chem.PropertyPickleOptions.PrivateProps, 278 ) 279 280 return [MolIndex, EncodedMol, EncodedStandardizedMol, StandardizationStatus] 281 282 283 def PerformStandardization(Mol, MolNum): 284 """Perform standardization and return a standardized mol along with the status of 285 the standardization.""" 286 287 # Track molname for its restoration after the standardization. RDKit standardization 288 # functions might mangle molname for molecules containing disconnected components... 289 MolName = Mol.GetProp("_Name") if Mol.HasProp("_Name") else None 290 291 StandardizedMol = Mol 292 try: 293 # Step 1: Cleanup... 294 if OptionsInfo["MethodologyParams"]["Cleanup"]: 295 StandardizedMol = CleanupMolecule(StandardizedMol) 296 297 # Step2: Get largest fragment... 298 if OptionsInfo["MethodologyParams"]["RemoveFragments"]: 299 StandardizedMol = ChooseLargestMoleculeFragment(StandardizedMol) 300 301 # Step3: Neutralize... 302 if OptionsInfo["MethodologyParams"]["Neutralize"]: 303 StandardizedMol = NeutralizeMolecule(StandardizedMol) 304 305 # Step4: Canonicalize tautomer... 306 if OptionsInfo["MethodologyParams"]["CanonicalizeTautomer"]: 307 StandardizedMol = CanonicalizeMoleculeTautomer(StandardizedMol) 308 309 Status = True 310 except Exception as ErrMsg: 311 StandardizedMol = None 312 if not OptionsInfo["QuietMode"]: 313 MiscUtil.PrintWarning("Failed to standardize molecule %s: %s" % (RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)) 314 Status = False 315 316 # Restore molname... 317 if MolName is not None: 318 if StandardizedMol is not None: 319 StandardizedMol.SetProp("_Name", MolName) 320 321 return (StandardizedMol, Status) 322 323 324 def CleanupMolecule(Mol): 325 """Clean up molecule.""" 326 327 if OptionsInfo["StandardizeParams"]["CleanupRemoveHydrogens"]: 328 Mol = Chem.RemoveHs(Mol) 329 330 if OptionsInfo["StandardizeParams"]["CleanupDisconnectMetals"]: 331 # Disconnect metal atoms that are defined as covalently bonded to non-metals... 332 Mol = OptionsInfo["StandardizeObjects"]["MetalDisconnector"].Disconnect(Mol) 333 334 if OptionsInfo["StandardizeParams"]["CleanupNormalize"]: 335 # Apply normalization transforms to correct functional groups and recombine charges... 336 Mol = rdMolStandardize.Normalize(Mol, OptionsInfo["CleanupParams"]) 337 338 if OptionsInfo["StandardizeParams"]["CleanupReionize"]: 339 # Ensure the strongest acid groups ionize first in partially ionized molecules... 340 Mol = rdMolStandardize.Reionize(Mol, OptionsInfo["CleanupParams"]) 341 342 if OptionsInfo["StandardizeParams"]["CleanupAssignStereo"]: 343 # Assign stereochemistry 344 Chem.AssignStereochemistry( 345 Mol, 346 force=OptionsInfo["StandardizeParams"]["CleanupAssignStereoForce"], 347 cleanIt=OptionsInfo["StandardizeParams"]["CleanupAssignStereoCleanIt"], 348 ) 349 350 Mol.UpdatePropertyCache() 351 352 return Mol 353 354 355 def ChooseLargestMoleculeFragment(Mol): 356 """Choose largest molecule fragment.""" 357 358 return OptionsInfo["StandardizeObjects"]["LargestFragmentChooser"].choose(Mol) 359 360 361 def NeutralizeMolecule(Mol): 362 """Neutralize molecule.""" 363 364 return OptionsInfo["StandardizeObjects"]["Uncharger"].uncharge(Mol) 365 366 367 def CanonicalizeMoleculeTautomer(Mol): 368 """Canonicalize molecule tautomer.""" 369 370 return OptionsInfo["StandardizeObjects"]["TautomerEnumerator"].Canonicalize(Mol) 371 372 373 def SetupStandardize(): 374 """Setup RDKit standardize objects to perform standardization.""" 375 376 OptionsInfo["StandardizeObjects"] = {} 377 378 OptionsInfo["CleanupParams"] = SetupStandardizeCleanupParameters() 379 380 if OptionsInfo["MethodologyParams"]["Cleanup"]: 381 if OptionsInfo["StandardizeParams"]["CleanupDisconnectMetals"]: 382 OptionsInfo["StandardizeObjects"]["MetalDisconnector"] = rdMolStandardize.MetalDisconnector() 383 384 if OptionsInfo["MethodologyParams"]["RemoveFragments"]: 385 OptionsInfo["StandardizeObjects"]["LargestFragmentChooser"] = rdMolStandardize.LargestFragmentChooser( 386 OptionsInfo["CleanupParams"] 387 ) 388 389 if OptionsInfo["MethodologyParams"]["Neutralize"]: 390 OptionsInfo["StandardizeObjects"]["Uncharger"] = rdMolStandardize.Uncharger( 391 OptionsInfo["CleanupParams"].doCanonical 392 ) 393 394 if OptionsInfo["MethodologyParams"]["CanonicalizeTautomer"]: 395 OptionsInfo["StandardizeObjects"]["TautomerEnumerator"] = rdMolStandardize.TautomerEnumerator( 396 OptionsInfo["CleanupParams"] 397 ) 398 399 400 def SetupStandardizeCleanupParameters(): 401 """Setup standardize clean up parameters for RDKit.""" 402 403 CleanupParams = rdMolStandardize.CleanupParameters() 404 StandardizeParams = OptionsInfo["StandardizeParams"] 405 406 if StandardizeParams["AcidBaseFile"] is not None: 407 CleanupParams.acidbaseFile = StandardizeParams["AcidBaseFile"] 408 if StandardizeParams["FragmentFile"] is not None: 409 CleanupParams.acidbaseFile = StandardizeParams["FragmentFile"] 410 if StandardizeParams["NormalizationsFile"] is not None: 411 CleanupParams.normalizationsFile = StandardizeParams["NormalizationsFile"] 412 if StandardizeParams["TautomerTransformsFile"] is not None: 413 CleanupParams.tautomerTransformsFile = StandardizeParams["TautomerTransformsFile"] 414 415 CleanupParams.maxRestarts = StandardizeParams["CleanupNormalizeMaxRestarts"] 416 417 CleanupParams.doCanonical = StandardizeParams["DoCanonical"] 418 419 CleanupParams.largestFragmentChooserUseAtomCount = StandardizeParams["LargestFragmentChooserUseAtomCount"] 420 CleanupParams.largestFragmentChooserCountHeavyAtomsOnly = StandardizeParams[ 421 "LargestFragmentChooserCountHeavyAtomsOnly" 422 ] 423 424 CleanupParams.preferOrganic = StandardizeParams["PreferOrganic"] 425 426 CleanupParams.maxTautomers = StandardizeParams["MaxTautomers"] 427 CleanupParams.maxTransforms = StandardizeParams["MaxTransforms"] 428 CleanupParams.tautomerRemoveBondStereo = StandardizeParams["TautomerRemoveBondStereo"] 429 CleanupParams.tautomerRemoveIsotopicHs = StandardizeParams["TautomerRemoveIsotopicHs"] 430 CleanupParams.tautomerRemoveSp3Stereo = StandardizeParams["TautomerRemoveSp3Stereo"] 431 CleanupParams.tautomerReassignStereo = StandardizeParams["TautomerReassignStereo"] 432 433 return CleanupParams 434 435 436 def WriteMolecule(Writer, Mol, Compute2DCoords): 437 """Write out molecule.""" 438 439 if OptionsInfo["CountMode"]: 440 return 441 442 if Compute2DCoords: 443 AllChem.Compute2DCoords(Mol) 444 445 Writer.write(Mol) 446 447 448 def SetupMoleculeWriter(): 449 """Setup a molecule writer.""" 450 451 Writer = None 452 if OptionsInfo["CountMode"]: 453 return Writer 454 455 Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"]) 456 if Writer is None: 457 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"]) 458 MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"]) 459 460 return Writer 461 462 463 def ProcessMethodologyParameters(): 464 """Process methodology parameters.""" 465 466 ParamsDefaultInfo = { 467 "Cleanup": ["bool", True], 468 "RemoveFragments": ["bool", True], 469 "Neutralize": ["bool", True], 470 "CanonicalizeTautomer": ["bool", True], 471 } 472 OptionsInfo["MethodologyParams"] = MiscUtil.ProcessOptionNameValuePairParameters( 473 "--methodologyParams", Options["--methodologyParams"], ParamsDefaultInfo 474 ) 475 476 477 def ProcessStandardizationParameters(): 478 """Process standardization parameters.""" 479 480 ParamsDefaultInfo = { 481 "AcidBaseFile": ["file", None], 482 "FragmentFile": ["file", None], 483 "NormalizationsFile": ["file", None], 484 "TautomerTransformsFile": ["file", None], 485 "CleanupRemoveHydrogens": ["bool", True], 486 "CleanupDisconnectMetals": ["bool", True], 487 "CleanupNormalize": ["bool", True], 488 "CleanupNormalizeMaxRestarts": ["int", 200], 489 "CleanupReionize": ["bool", True], 490 "CleanupAssignStereo": ["bool", True], 491 "CleanupAssignStereoCleanIt": ["bool", True], 492 "CleanupAssignStereoForce": ["bool", True], 493 "DoCanonical": ["bool", True], 494 "LargestFragmentChooserUseAtomCount": ["bool", True], 495 "LargestFragmentChooserCountHeavyAtomsOnly": ["bool", False], 496 "PreferOrganic": ["bool", False], 497 "MaxTautomers": ["int", 1000], 498 "MaxTransforms": ["int", 1000], 499 "TautomerRemoveBondStereo": ["bool", True], 500 "TautomerRemoveIsotopicHs": ["bool", True], 501 "TautomerRemoveSp3Stereo": ["bool", True], 502 "TautomerReassignStereo": ["bool", True], 503 } 504 505 OptionsInfo["StandardizeParams"] = MiscUtil.ProcessOptionNameValuePairParameters( 506 "--standardizeParams", Options["--standardizeParams"], ParamsDefaultInfo 507 ) 508 509 # Validate numerical values... 510 for ParamName in ["CleanupNormalizeMaxRestarts", "MaxTautomers", "MaxTransforms"]: 511 ParamValue = OptionsInfo["StandardizeParams"][ParamName] 512 if ParamValue <= 0: 513 MiscUtil.PrintError( 514 'The parameter value, %s, specified for parameter name, %s, using "-s, --standardizeParams" option is not a valid value. Supported values: > 0' 515 % (ParamValue, ParamName) 516 ) 517 518 519 def ProcessOptions(): 520 """Process and validate command line arguments and options.""" 521 522 MiscUtil.PrintInfo("Processing options...") 523 524 # Validate options... 525 ValidateOptions() 526 527 OptionsInfo["Infile"] = Options["--infile"] 528 ParamsDefaultInfoOverride = {"RemoveHydrogens": False} 529 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters( 530 "--infileParams", Options["--infileParams"], Options["--infile"], ParamsDefaultInfo=ParamsDefaultInfoOverride 531 ) 532 533 OptionsInfo["Outfile"] = Options["--outfile"] 534 OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters( 535 "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"] 536 ) 537 538 OptionsInfo["Overwrite"] = Options["--overwrite"] 539 540 OptionsInfo["CountMode"] = False 541 if re.match("^count$", Options["--mode"], re.I): 542 OptionsInfo["CountMode"] = True 543 544 OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False 545 OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"]) 546 547 OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False 548 549 ProcessMethodologyParameters() 550 ProcessStandardizationParameters() 551 552 553 def RetrieveOptions(): 554 """Retrieve command line arguments and options.""" 555 556 # Get options... 557 global Options 558 Options = docopt(_docoptUsage_) 559 560 # Set current working directory to the specified directory... 561 WorkingDir = Options["--workingdir"] 562 if WorkingDir: 563 os.chdir(WorkingDir) 564 565 # Handle examples option... 566 if "--examples" in Options and Options["--examples"]: 567 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_)) 568 sys.exit(0) 569 570 571 def ValidateOptions(): 572 """Validate option values.""" 573 574 MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"]) 575 MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv") 576 577 if Options["--outfile"]: 578 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi") 579 MiscUtil.ValidateOptionsOutputFileOverwrite( 580 "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"] 581 ) 582 MiscUtil.ValidateOptionsDistinctFileNames( 583 "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"] 584 ) 585 586 MiscUtil.ValidateOptionTextValue("--mode", Options["--mode"], "standardize count") 587 if re.match("^standardize$", Options["--mode"], re.I): 588 if not Options["--outfile"]: 589 MiscUtil.PrintError( 590 'The outfile must be specified using "-o, --outfile" during "standardize" value of "--mode" option' 591 ) 592 593 MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no") 594 MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no") 595 596 597 # Setup a usage string for docopt... 598 _docoptUsage_ = """ 599 RDKitStandardizeMolecules.py - Standardize molecules 600 601 Usage: 602 RDKitStandardizeMolecules.py [--infileParams <Name,Value,...>] [--methodologyParams <Name,Value,...>] 603 [--mode <standardize or count>] [--mp <yes or no>] [--mpParams <Name,Value,...>] 604 [--outfileParams <Name,Value,...> ] [--overwrite] [--standardizeParams <Name,Value,...>] 605 [--quiet <yes or no>] [-w <dir>] [-o <outfile>] -i <infile> 606 RDKitStandardizeMolecules.py -h | --help | -e | --examples 607 608 Description: 609 Standardize molecules and write them out to an output file or simply count 610 the number of molecules to be standardized. The standardization methodology 611 consists of the following 4 steps executed in a sequential manner: 612 613 1. Cleanup molecules 614 2. Keep largest fragment 615 3. Neutralize molecules 616 4. Select canonical tautomer 617 618 The molecules are cleaned up by performing the following actions: 619 620 1. Remove hydrogens 621 2. Disconnect metal atoms - Disconnect metal atoms covalently bonded 622 to non-metals 623 3. Normalize - Normalize functional groups and recombine charges 624 4. Reionize - Ionize strongest acid groups first in partially 625 ionized molecules 626 5. Assign stereochemistry 627 628 You may optionally skip any cleanup action during standardization. 629 630 The supported input file formats are: SD (.sdf, .sd), SMILES (.smi., csv, .tsv, .txt) 631 632 The supported output file formats are: SD (.sdf, .sd), SMILES (.smi) 633 634 Options: 635 -e, --examples 636 Print examples. 637 -h, --help 638 Print this help message. 639 -i, --infile <infile> 640 Input file name. 641 --infileParams <Name,Value,...> [default: auto] 642 A comma delimited list of parameter name and value pairs for reading 643 molecules from files. The supported parameter names for different file 644 formats, along with their default values, are shown below: 645 646 SD, MOL: removeHydrogens,no,sanitize,yes,strictParsing,yes 647 SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space, 648 smilesTitleLine,auto,sanitize,yes 649 650 Possible values for smilesDelimiter: space, comma or tab. 651 -m, --mode <standardize or count> [default: standardize] 652 Specify whether to standardize molecules and write them out or simply 653 count the number of molecules being standardized. 654 --methodologyParams <Name,Value,...> [default: auto] 655 A comma delimited list of parameter name and value pairs to control 656 the execution of different steps in the standardization methodology. The 657 supported parameter names along with their default values are shown 658 below: 659 660 cleanup,yes,removeFragments,yes,neutralize,yes, 661 canonicalizeTautomer,yes 662 663 The standardization methodology consists of the following 4 steps executed 664 in a sequential manner starting from step 1: 665 666 1. cleanup 667 2. removeFragments 668 3. neutralize 669 4. canonicalizeTautomer 670 671 You may optionally skip the execution of any standardization step. 672 673 The step1, cleanup, performs the following actions: 674 675 1. Remove hydrogens 676 2. Disconnect metal atoms - Disconnect metal atoms covalently bonded 677 to non-metals 678 3. Normalize - Normalize functional groups and recombine charges 679 4. Reionize - Ionize strongest acid groups first in partially 680 ionized molecules 681 5. Assign stereochemistry 682 683 You may optionally skip any cleanup action using '-s, --standardize' option. 684 685 The step2, removeFragments, employs rdMolStandardize.FragmentParent() 686 function to keep the largest fragment. 687 688 The step3, neutralize, uses rdMolStandardize.Uncharger().uncharge() 689 function to neutralize molecules by adding/removing hydrogens. 690 691 The step4, canonicalizeTautomer, relies on Canonicalize() function availabe via 692 rdMolStandardize.TautomerEnumerator() to select a canonical tautomer. 693 --mp <yes or no> [default: no] 694 Use multiprocessing. 695 696 By default, input data is retrieved in a lazy manner via mp.Pool.imap() 697 function employing lazy RDKit data iterable. This allows processing of 698 arbitrary large data sets without any additional requirements memory. 699 700 All input data may be optionally loaded into memory by mp.Pool.map() 701 before starting worker processes in a process pool by setting the value 702 of 'inputDataMode' to 'InMemory' in '--mpParams' option. 703 704 A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input 705 data mode may adversely impact the performance. The '--mpParams' section 706 provides additional information to tune the value of 'chunkSize'. 707 --mpParams <Name,Value,...> [default: auto] 708 A comma delimited list of parameter name and value pairs to configure 709 multiprocessing. 710 711 The supported parameter names along with their default and possible 712 values are shown below: 713 714 chunkSize, auto 715 inputDataMode, Lazy [ Possible values: InMemory or Lazy ] 716 numProcesses, auto [ Default: mp.cpu_count() ] 717 718 These parameters are used by the following functions to configure and 719 control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and 720 mp.Pool.imap(). 721 722 The chunkSize determines chunks of input data passed to each worker 723 process in a process pool by mp.Pool.map() and mp.Pool.imap() functions. 724 The default value of chunkSize is dependent on the value of 'inputDataMode'. 725 726 The mp.Pool.map() function, invoked during 'InMemory' input data mode, 727 automatically converts RDKit data iterable into a list, loads all data into 728 memory, and calculates the default chunkSize using the following method 729 as shown in its code: 730 731 chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4) 732 if extra: chunkSize += 1 733 734 For example, the default chunkSize will be 7 for a pool of 4 worker processes 735 and 100 data items. 736 737 The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs 738 'lazy' RDKit data iterable to retrieve data as needed, without loading all the 739 data into memory. Consequently, the size of input data is not known a priori. 740 It's not possible to estimate an optimal value for the chunkSize. The default 741 chunkSize is set to 1. 742 743 The default value for the chunkSize during 'Lazy' data mode may adversely 744 impact the performance due to the overhead associated with exchanging 745 small chunks of data. It is generally a good idea to explicitly set chunkSize to 746 a larger value during 'Lazy' input data mode, based on the size of your input 747 data and number of processes in the process pool. 748 749 The mp.Pool.map() function waits for all worker processes to process all 750 the data and return the results. The mp.Pool.imap() function, however, 751 returns the the results obtained from worker processes as soon as the 752 results become available for specified chunks of data. 753 754 The order of data in the results returned by both mp.Pool.map() and 755 mp.Pool.imap() functions always corresponds to the input data. 756 -o, --outfile <outfile> 757 Output file name. 758 --outfileParams <Name,Value,...> [default: auto] 759 A comma delimited list of parameter name and value pairs for writing 760 molecules to files. The supported parameter names for different file 761 formats, along with their default values, are shown below: 762 763 SD: compute2DCoords,auto,kekulize,yes,forceV3000,no 764 SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes, 765 smilesTitleLine,yes,smilesMolName,yes,smilesMolProps,no 766 767 Default value for compute2DCoords: yes for SMILES input file; no for all other 768 file types. 769 --overwrite 770 Overwrite existing files. 771 -q, --quiet <yes or no> [default: no] 772 Use quiet mode. The warning and information messages will not be printed. 773 -s, --standardizeParams <Name,Value,...> [default: auto] 774 A comma delimited list of parameter name and value pairs for standardizing 775 molecules. The supported parameter names along with their default values 776 are shown below: 777 778 acidbaseFile,none,fragmentFile,none,normalizationsFile,none, 779 tautomerTransformsFile,none, 780 cleanupRemoveHydrogens,yes,cleanupDisconnectMetals,yes, 781 cleanupNormalize,yes,cleanupNormalizeMaxRestarts,200, 782 cleanupReionize,yes,cleanupAssignStereo,yes, 783 cleanupAssignStereoCleanIt,yes,cleanupAssignStereoForce,yes 784 largestFragmentChooserUseAtomCount,yes, 785 largestFragmentChooserCountHeavyAtomsOnly,no,preferOrganic,no, 786 doCanonical,yes, 787 maxTautomers,1000,maxTransforms,1000, 788 tautomerRemoveBondStereo,yes,tautomerRemoveIsotopicHs,yes 789 tautomerRemoveSp3Stereo,yes,tautomerReassignStereo,yes 790 791 A brief description of the standardization parameters, taken from RDKit 792 documentation, is as follows: 793 794 acidbaseFile - File containing acid and base definitions 795 fragmentFile - File containing fragment definitions 796 normalizationsFile - File conataining normalization transformations 797 tautomerTransformsFile - File containing tautomer transformations 798 799 cleanupRemoveHydrogens - Remove hydrogens druring cleanup 800 cleanupDisconnectMetals - Disconnect metal atoms covalently bonded 801 to non-metals during cleanup 802 cleanupNormalize - Normalize functional groups and recombine 803 charges during cleanup 804 cleanupNormalizeMaxRestarts - Maximum number of restarts during 805 normalization step of cleanup 806 cleanupReionize -Ionize strongest acid groups first in partially 807 ionized molecules during cleanup 808 cleanupAssignStereo - Assign stererochemistry during cleanup 809 cleanupAssignStereoCleanIt - Clean property _CIPCode during 810 assign stereochemistry 811 cleanupAssignStereoForce - Always perform stereochemistry 812 calculation during assign stereochemistry 813 814 largestFragmentChooserUseAtomCount - Use atom count as main 815 criterion before molecular weight to determine largest fragment 816 in LargestFragmentChooser 817 largestFragmentChooserCountHeavyAtomsOnly - Count only heavy 818 atoms to determine largest fragment in LargestFragmentChooser 819 preferOrganic - Prefer organic fragments over inorganic ones when 820 choosing fragments 821 822 doCanonical - Apply atom-order dependent normalizations in a 823 canonical order during uncharging 824 825 maxTautomers - Maximum number of tautomers to generate 826 maxTransforms - Maximum number of transforms to apply during 827 tautomer enumeration 828 tautomerRemoveBondStereo - Remove stereochemistry from double bonds 829 involved in tautomerism 830 tautomerRemoveIsotopicHs: Remove isotopic Hs from centers involved in tautomerism 831 tautomerRemoveSp3Stereo - Remove stereochemistry from sp3 centers 832 involved in tautomerism 833 tautomerReassignStereo - AssignStereochemistry on all generated tautomers 834 835 The default value is set to none for the following file name parameters: 836 acidbaseFile, fragmentFile, normalizationsFile, and tautomerTransformsFile. 837 The script relies on RDKit to automatically load appropriate acid base and 838 fragment definitions along with normalization and tautomer transformations 839 from a set of internal catalogs. 840 841 Note: The fragmentFile doesn't appear to be used by the RDKit method 842 rdMolStandardize.FragmentParent() to find largest fragment. 843 844 The contents of various standardization definitions and transformations files 845 are described below: 846 847 acidbaseFile - File containing acid and base definitions 848 849 // Name Acid Base 850 -OSO3H OS(=O)(=O)[OH] OS(=O)(=O)[O-] 851 -SO3H [!O]S(=O)(=O)[OH] [!O]S(=O)(=O)[O-] 852 -OSO2H O[SD3](=O)[OH] O[SD3](=O)[O-] 853 ... ... ... 854 855 fragmentFile - File containing fragment definitions 856 857 // Name SMARTS 858 hydrogen [H] 859 fluorine [F] 860 chlorine [Cl] 861 ... ... ... 862 863 normalizationsFile - File conataining normalization transformations 864 865 // Name SMIRKS 866 Sulfone to S(=O)(=O) [S+2:1]([O-:2])([O-:3])>> 867 [S+0:1](=[O-0:2])(=[O-0:3]) 868 Pyridine oxide to n+O- [n:1]=[O:2]>>[n+:1][O-:2] 869 ... ... ... 870 871 tautomerTransformsFile - File containing tautomer transformations 872 873 // Name SMARTS Bonds Charges 874 1,3 (thio)keto/enol f [CX4!H0]-[C]=[O,S,Se,Te;X1] 875 1,3 (thio)keto/enol r [O,S,Se,Te;X2!H0]-[C]=[C] 876 1,5 (thio)keto/enol f [CX4,NX3;!H0]-[C]=[C][CH0]=[O,S,Se,Te;X1] 877 ... ... ... 878 879 -w, --workingdir <dir> 880 Location of working directory which defaults to the current directory. 881 882 Examples: 883 To standardize molecules in a SMILES file by executing all standardization 884 steps and write out a SMILES file, type: 885 886 % RDKitStandardizeMolecules.py -i Sample.smi -o SampleOut.smi 887 888 To standardize molecules in a SD file by executing all standardization 889 steps, performing standardization in multiprocessing mode on all available 890 CPUs without loading all data into memory, and write out and write out a 891 SD file, type: 892 893 % RDKitStandardizeMolecules.py --mp yes -i Sample.sdf -o SampleOut.sdf 894 895 To standardize molecules in a SMILES file by executing all standardization 896 steps, performing standardization in multiprocessing mode on all available 897 CPUs by loading all data into memory, and write out and write out a 898 SMILES file, type: 899 900 % RDKitStandardizeMolecules.py --mp yes --mpParams "inputDataMode, 901 InMemory" -i Sample.smi -o SampleOut.smi 902 903 To standardize molecules in a SMILES file by executing all standardization 904 steps, performing standardization in multiprocessing mode on specific number 905 of CPUs and chunk size without loading all data into memory, and write out a 906 a SMILES file, type: 907 908 % RDKitStandardizeMolecules.py --mp yes --mpParams "inputDataMode,Lazy, 909 numProcesses,4,chunkSize,8" -i Sample.smi -o SampleOut.smi 910 911 To count number of molecules to be standardized without generating any 912 output file, type: 913 914 % RDKitStandardizeMolecules.py -m count -i Sample.sdf 915 916 To standardize molecules in a SD file by executing specific standardization 917 steps along with explicit values for various parameters to control the 918 standardization behavior, and write out a SD file, type: 919 920 % RDKitStandardizeMolecules.py --methodologyParams "cleanup,yes, 921 removeFragments,yes,neutralize,yes,canonicalizeTautomer,yes" 922 --standardizeParams "cleanupRemoveHydrogens,yes, 923 cleanupDisconnectMetals,yes,cleanupNormalize,yes, 924 cleanupNormalizeMaxRestarts,200,cleanupReionize,yes, 925 cleanupAssignStereo,yes,largestFragmentChooserUseAtomCount,yes, 926 doCanonical,yes,maxTautomers,1000" 927 -i Sample.sdf -o SampleOut.sdf 928 929 To standardize molecules in a CSV SMILES file, SMILES strings in column 1, 930 name in column 2, and generate output SD file, type: 931 932 % RDKitStandardizeMolecules.py --infileParams 933 "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1, 934 smilesNameColumn,2" --outfileParams "compute2DCoords,yes" 935 -i SampleSMILES.csv -o SampleOut.sdf 936 937 Author: 938 Manish Sud(msud@san.rr.com) 939 940 See also: 941 RDKitConvertFileFormat.py, RDKitEnumerateTautomers.py, 942 RDKitRemoveDuplicateMolecules.py, RDKitRemoveInvalidMolecules.py, 943 RDKitRemoveSalts.py, RDKitSearchFunctionalGroups.py, RDKitSearchSMARTS.py 944 945 Copyright: 946 Copyright (C) 2026 Manish Sud. All rights reserved. 947 948 The functionality available in this script is implemented using RDKit, an 949 open source toolkit for cheminformatics developed by Greg Landrum. 950 951 This file is part of MayaChemTools. 952 953 MayaChemTools is free software; you can redistribute it and/or modify it under 954 the terms of the GNU Lesser General Public License as published by the Free 955 Software Foundation; either version 3 of the License, or (at your option) any 956 later version. 957 958 """ 959 960 if __name__ == "__main__": 961 main()