MayaChemTools

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