MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: RDKitRemoveSalts.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.SaltRemover import SaltRemover
  42     from rdkit.Chem.SaltRemover import InputFormat
  43     from rdkit.Chem import AllChem
  44 except ImportError as ErrMsg:
  45     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
  46     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
  47     sys.exit(1)
  48 
  49 # MayaChemTools imports...
  50 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  51 try:
  52     from docopt import docopt
  53     import MiscUtil
  54     import RDKitUtil
  55 except ImportError as ErrMsg:
  56     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  57     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  58     sys.exit(1)
  59 
  60 ScriptName = os.path.basename(sys.argv[0])
  61 Options = {}
  62 OptionsInfo = {}
  63 
  64 
  65 def main():
  66     """Start execution of the script."""
  67 
  68     MiscUtil.PrintInfo(
  69         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
  70         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
  71     )
  72 
  73     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  74 
  75     # Retrieve command line arguments and options...
  76     RetrieveOptions()
  77 
  78     # Process and validate command line arguments and options...
  79     ProcessOptions()
  80 
  81     # Perform actions required by the script...
  82     RemoveSalts()
  83 
  84     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  85     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  86 
  87 
  88 def RemoveSalts():
  89     """Identify and remove salts from molecules."""
  90 
  91     # Setup a molecule reader...
  92     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
  93     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
  94 
  95     # Set up a molecule writer...
  96     Writer = SetupMoleculeWriter()
  97 
  98     MolCount, ValidMolCount, SaltsMolCount = ProcessMolecules(Mols, Writer)
  99 
 100     if Writer is not None:
 101         Writer.close()
 102 
 103     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
 104     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
 105     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
 106 
 107     MiscUtil.PrintInfo("\nNumber of molecules containing salts: %d" % (SaltsMolCount))
 108 
 109 
 110 def ProcessMolecules(Mols, Writer):
 111     """Process and remove salts from 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 remove salts from molecules using a single process."""
 121 
 122     MiscUtil.PrintInfo("\nRemoving salts...")
 123 
 124     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 125     SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"]
 126 
 127     # Set up a salt remover...
 128     Remover = SetupSaltRemover()
 129 
 130     (MolCount, ValidMolCount, SaltsMolCount) = [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             MolName = RDKitUtil.GetMolName(Mol, MolCount)
 140             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 141             continue
 142 
 143         ValidMolCount += 1
 144         if FirstMol:
 145             FirstMol = False
 146             if SetSMILESMolProps:
 147                 RDKitUtil.SetWriterMolProps(Writer, Mol)
 148 
 149         UnsaltedMol, SaltyStatus = RemoveMolSalts(Mol, Remover, MolCount)
 150 
 151         if SaltyStatus:
 152             SaltsMolCount += 1
 153 
 154         WriteMolecule(Writer, UnsaltedMol, Compute2DCoords)
 155 
 156     return (MolCount, ValidMolCount, SaltsMolCount)
 157 
 158 
 159 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer):
 160     """Process and remove salts from molecules using  multiprocessing."""
 161 
 162     MiscUtil.PrintInfo("\nRemoving salts using multiprocessing...")
 163 
 164     MPParams = OptionsInfo["MPParams"]
 165     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 166 
 167     # Setup data for initializing a worker process...
 168     InitializeWorkerProcessArgs = (
 169         MiscUtil.ObjectToBase64EncodedString(Options),
 170         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
 171     )
 172 
 173     # Setup a encoded mols data iterable for a worker process by pickling only public
 174     # and private molecule properties...
 175     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
 176 
 177     # Setup process pool along with data initialization for each process...
 178     MiscUtil.PrintInfo(
 179         "\nConfiguring multiprocessing using %s method..."
 180         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
 181     )
 182     MiscUtil.PrintInfo(
 183         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
 184         % (
 185             MPParams["NumProcesses"],
 186             MPParams["InputDataMode"],
 187             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
 188         )
 189     )
 190 
 191     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
 192 
 193     # Start processing...
 194     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
 195         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
 196     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
 197         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
 198     else:
 199         MiscUtil.PrintError(
 200             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
 201         )
 202 
 203     SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"]
 204 
 205     (MolCount, ValidMolCount, SaltsMolCount) = [0] * 3
 206     FirstMol = True
 207     for Result in Results:
 208         MolCount += 1
 209         MolIndex, EncodedMol, SaltyStatus = Result
 210 
 211         if EncodedMol is None:
 212             continue
 213         ValidMolCount += 1
 214 
 215         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
 216 
 217         if FirstMol:
 218             FirstMol = False
 219             if SetSMILESMolProps:
 220                 RDKitUtil.SetWriterMolProps(Writer, Mol)
 221 
 222         if SaltyStatus:
 223             SaltsMolCount += 1
 224 
 225         WriteMolecule(Writer, Mol, Compute2DCoords)
 226 
 227     return (MolCount, ValidMolCount, SaltsMolCount)
 228 
 229 
 230 def InitializeWorkerProcess(*EncodedArgs):
 231     """Initialize data for a worker process."""
 232 
 233     global Options, OptionsInfo
 234 
 235     MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
 236 
 237     # Decode Options and OptionInfo...
 238     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
 239     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
 240 
 241     # Set up salt remover...
 242     OptionsInfo["SaltRemover"] = SetupSaltRemover()
 243 
 244 
 245 def WorkerProcess(EncodedMolInfo):
 246     """Process data for a worker process."""
 247 
 248     MolIndex, EncodedMol = EncodedMolInfo
 249 
 250     if EncodedMol is None:
 251         return [MolIndex, None, False]
 252 
 253     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
 254     if RDKitUtil.IsMolEmpty(Mol):
 255         MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
 256         MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 257         return [MolIndex, None, False]
 258 
 259     Mol, SaltyStatus = RemoveMolSalts(Mol, OptionsInfo["SaltRemover"], (MolIndex + 1))
 260     EncodedMol = RDKitUtil.MolToBase64EncodedMolString(
 261         Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps
 262     )
 263 
 264     return [MolIndex, EncodedMol, SaltyStatus]
 265 
 266 
 267 def RemoveMolSalts(Mol, Remover, MolNum):
 268     """Remove salts from mol and return unsalted mol along with mol salty status."""
 269 
 270     UnsaltedMol = Mol
 271     SaltyStatus = False
 272 
 273     if Remover is not None:
 274         KeptMol, DeletedMols = Remover.StripMolWithDeleted(Mol, dontRemoveEverything=False)
 275         if len(DeletedMols) >= 1:
 276             SaltyStatus = True
 277         if RDKitUtil.IsMolEmpty(KeptMol):
 278             if len(DeletedMols) >= 1:
 279                 # Take the larged fragment from DeletedMols
 280                 UnsaltedMol = GetLargestMol(DeletedMols)
 281     else:
 282         # Use largest fragment as unsalted molecule...
 283         MolFrags = Chem.GetMolFrags(Mol, asMols=True)
 284         if len(MolFrags) > 1:
 285             # Keep the largest fragment as unsalted molecule...
 286             SaltyStatus = True
 287             UnsaltedMol = GetLargestMol(MolFrags)
 288 
 289     if SaltyStatus:
 290         Chem.SanitizeMol(UnsaltedMol)
 291         MolName = RDKitUtil.GetMolName(Mol, MolNum)
 292         if len(MolName):
 293             UnsaltedMol.SetProp("_Name", MolName)
 294 
 295         # Set mol properties...
 296         for DataLabel in Mol.GetPropNames(includePrivate=False, includeComputed=False):
 297             DataProp = Mol.GetProp(DataLabel)
 298             UnsaltedMol.SetProp(DataLabel, DataProp)
 299 
 300     return (UnsaltedMol, SaltyStatus)
 301 
 302 
 303 def GetLargestMol(Mols):
 304     """Get largest mol from list of mols."""
 305 
 306     LargestMol = None
 307     LargestMolSize = -1
 308     for Mol in Mols:
 309         Size = Mol.GetNumAtoms()
 310         if Size > LargestMolSize:
 311             LargestMol = Mol
 312             LargestMolSize = Size
 313 
 314     return LargestMol
 315 
 316 
 317 def SetupSaltRemover():
 318     """Setup a salt remover."""
 319 
 320     Remover = None
 321     if OptionsInfo["SaltsByComponentsMode"]:
 322         return Remover
 323 
 324     return SaltRemover(
 325         defnFilename=OptionsInfo["SaltsFile"], defnData=OptionsInfo["SaltsSMARTS"], defnFormat=InputFormat.SMARTS
 326     )
 327 
 328 
 329 def WriteMolecule(Writer, Mol, Compute2DCoords):
 330     """Write out molecule."""
 331 
 332     if OptionsInfo["CountMode"]:
 333         return
 334 
 335     if Compute2DCoords:
 336         AllChem.Compute2DCoords(Mol)
 337 
 338     Writer.write(Mol)
 339 
 340 
 341 def SetupMoleculeWriter():
 342     """Setup a molecule writer."""
 343 
 344     Writer = None
 345     if OptionsInfo["CountMode"]:
 346         return Writer
 347 
 348     Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
 349     if Writer is None:
 350         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
 351     MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"])
 352 
 353     return Writer
 354 
 355 
 356 def ProcessOptions():
 357     """Process and validate command line arguments and options."""
 358 
 359     MiscUtil.PrintInfo("Processing options...")
 360 
 361     # Validate options...
 362     ValidateOptions()
 363 
 364     OptionsInfo["Infile"] = Options["--infile"]
 365     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 366         "--infileParams", Options["--infileParams"], Options["--infile"]
 367     )
 368 
 369     OptionsInfo["Outfile"] = Options["--outfile"]
 370     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 371         "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"]
 372     )
 373 
 374     OptionsInfo["Overwrite"] = Options["--overwrite"]
 375 
 376     OptionsInfo["CountMode"] = False
 377     if re.match("^count$", Options["--mode"], re.I):
 378         OptionsInfo["CountMode"] = True
 379 
 380     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 381     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 382 
 383     SaltsByComponentsMode = False
 384     SaltsBySMARTSFileMode = False
 385     SaltsBySMARTSMode = False
 386     if re.match("^ByComponent$", Options["--saltsMode"], re.I):
 387         SaltsByComponentsMode = True
 388     elif re.match("^BySMARTSFile$", Options["--saltsMode"], re.I):
 389         SaltsBySMARTSFileMode = False
 390     elif re.match("^BySMARTS$", Options["--saltsMode"], re.I):
 391         SaltsBySMARTSMode = True
 392     else:
 393         MiscUtil.PrintError(
 394             'The salts mode specified, %s, using "--saltsMode" option is not valid.' % Options["--saltsMode"]
 395         )
 396     OptionsInfo["SaltsByComponentsMode"] = SaltsByComponentsMode
 397     OptionsInfo["SaltsBySMARTSFileMode"] = SaltsBySMARTSFileMode
 398     OptionsInfo["SaltsBySMARTSMode"] = SaltsBySMARTSMode
 399 
 400     SaltsFile = None
 401     if re.match("^BySMARTSFile$", Options["--saltsMode"], re.I):
 402         if not re.match("^auto$", Options["--saltsFile"], re.I):
 403             SaltsFile = Options["--saltsFile"]
 404     OptionsInfo["SaltsFile"] = SaltsFile
 405 
 406     SaltsSMARTS = None
 407     if re.match("^BySMARTS$", Options["--saltsMode"], re.I):
 408         if not Options["--saltsSMARTS"]:
 409             MiscUtil.PrintError(
 410                 'No salts SMARTS pattern specified using "--saltsSMARTS" option during "BySMARTS" value of "-s, --saltsMode" option'
 411             )
 412         SaltsSMARTS = Options["--saltsSMARTS"].strip(" ")
 413         if not len(SaltsSMARTS):
 414             MiscUtil.PrintError(
 415                 'Empty SMARTS pattern specified using "--saltsSMARTS" option during "BySMARTS" value of "-s, --saltsMode" option'
 416             )
 417         if re.search(" ", SaltsSMARTS):
 418             SaltsSMARTS = re.sub("[ ]+", "\n", SaltsSMARTS)
 419 
 420     OptionsInfo["SaltsSMARTS"] = SaltsSMARTS
 421 
 422 
 423 def RetrieveOptions():
 424     """Retrieve command line arguments and options."""
 425 
 426     # Get options...
 427     global Options
 428     Options = docopt(_docoptUsage_)
 429 
 430     # Set current working directory to the specified directory...
 431     WorkingDir = Options["--workingdir"]
 432     if WorkingDir:
 433         os.chdir(WorkingDir)
 434 
 435     # Handle examples option...
 436     if "--examples" in Options and Options["--examples"]:
 437         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 438         sys.exit(0)
 439 
 440 
 441 def ValidateOptions():
 442     """Validate option values."""
 443 
 444     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 445     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd smi txt csv tsv")
 446 
 447     if Options["--outfile"]:
 448         MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi")
 449         MiscUtil.ValidateOptionsOutputFileOverwrite(
 450             "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 451         )
 452         MiscUtil.ValidateOptionsDistinctFileNames(
 453             "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 454         )
 455 
 456     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "remove count")
 457     if re.match("^remove$", Options["--mode"], re.I):
 458         if not Options["--outfile"]:
 459             MiscUtil.PrintError(
 460                 'The outfile must be specified using "-o, --outfile" during "remove" value of "-m, --mode" option'
 461             )
 462 
 463     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 464 
 465     MiscUtil.ValidateOptionTextValue("--saltsMode", Options["--saltsMode"], "ByComponent BySMARTSFile BySMARTS")
 466 
 467     if re.match("^BySMARTSFile$", Options["--saltsMode"], re.I):
 468         if not re.match("^auto$", Options["--saltsFile"], re.I):
 469             MiscUtil.ValidateOptionFilePath("--saltsFile", Options["--saltsFile"])
 470 
 471 
 472 # Setup a usage string for docopt...
 473 _docoptUsage_ = """
 474 RDKitRemoveSalts.py - Remove salts
 475 
 476 Usage:
 477     RDKitRemoveSalts.py  [--infileParams <Name,Value,...>] [--mode <remove or count>]
 478                          [--mp <yes or no>] [--mpParams <Name,Value,...>] [--outfileParams <Name,Value,...> ]
 479                          [--overwrite] [--saltsMode <ByComponent, BySMARTSFile, BySMARTS>]
 480                          [--saltsFile <FileName or auto>] [--saltsSMARTS <SMARTS>]
 481                          [-w <dir>] [-o <outfile>]  -i <infile>
 482     RDKitRemoveSalts.py -h | --help | -e | --examples
 483 
 484 Description:
 485     Remove salts from molecules or simply count the number of molecules containing
 486     salts. Salts are identified and removed based on either SMARTS strings or by selecting
 487     the largest disconnected components in molecules as non-salt portion of molecules.
 488 
 489     The supported input file formats are: SD (.sdf, .sd), SMILES (.smi., csv, .tsv, .txt)
 490 
 491     The supported output file formats are: SD (.sdf, .sd), SMILES (.smi)
 492 
 493 Options:
 494     -e, --examples
 495         Print examples.
 496     -h, --help
 497         Print this help message.
 498     -i, --infile <infile>
 499         Input file name.
 500     --infileParams <Name,Value,...>  [default: auto]
 501         A comma delimited list of parameter name and value pairs for reading
 502         molecules from files. The supported parameter names for different file
 503         formats, along with their default values, are shown below:
 504             
 505             SD: removeHydrogens,yes,sanitize,yes,strictParsing,yes
 506             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 507                 smilesTitleLine,auto,sanitize,yes
 508             
 509         Possible values for smilesDelimiter: space, comma or tab.
 510     -m, --mode <remove or count>  [default: remove]
 511         Specify whether to remove salts from molecules and write out molecules
 512         or or simply count the number of molecules containing salts.
 513     --mp <yes or no>  [default: no]
 514         Use multiprocessing.
 515          
 516         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 517         function employing lazy RDKit data iterable. This allows processing of
 518         arbitrary large data sets without any additional requirements memory.
 519         
 520         All input data may be optionally loaded into memory by mp.Pool.map()
 521         before starting worker processes in a process pool by setting the value
 522         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 523         
 524         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 525         data mode may adversely impact the performance. The '--mpParams' section
 526         provides additional information to tune the value of 'chunkSize'.
 527     --mpParams <Name,Value,...>  [default: auto]
 528         A comma delimited list of parameter name and value pairs to configure
 529         multiprocessing.
 530         
 531         The supported parameter names along with their default and possible
 532         values are shown below:
 533         
 534             chunkSize, auto
 535             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 536             numProcesses, auto   [ Default: mp.cpu_count() ]
 537         
 538         These parameters are used by the following functions to configure and
 539         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 540         mp.Pool.imap().
 541         
 542         The chunkSize determines chunks of input data passed to each worker
 543         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 544         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 545         
 546         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 547         automatically converts RDKit data iterable into a list, loads all data into
 548         memory, and calculates the default chunkSize using the following method
 549         as shown in its code:
 550         
 551             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 552             if extra: chunkSize += 1
 553         
 554         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 555         and 100 data items.
 556         
 557         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 558         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 559         data into memory. Consequently, the size of input data is not known a priori.
 560         It's not possible to estimate an optimal value for the chunkSize. The default 
 561         chunkSize is set to 1.
 562         
 563         The default value for the chunkSize during 'Lazy' data mode may adversely
 564         impact the performance due to the overhead associated with exchanging
 565         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 566         a larger value during 'Lazy' input data mode, based on the size of your input
 567         data and number of processes in the process pool.
 568         
 569         The mp.Pool.map() function waits for all worker processes to process all
 570         the data and return the results. The mp.Pool.imap() function, however,
 571         returns the the results obtained from worker processes as soon as the
 572         results become available for specified chunks of data.
 573         
 574         The order of data in the results returned by both mp.Pool.map() and 
 575         mp.Pool.imap() functions always corresponds to the input data.
 576     -o, --outfile <outfile>
 577         Output file name.
 578     --outfileParams <Name,Value,...>  [default: auto]
 579         A comma delimited list of parameter name and value pairs for writing
 580         molecules to files. The supported parameter names for different file
 581         formats, along with their default values, are shown below:
 582             
 583             SD: compute2DCoords,auto,kekulize,yes,forceV3000,no
 584             SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes,
 585                 smilesTitleLine,yes,smilesMolName,yes,smilesMolProps,no
 586             
 587         Default value for compute2DCoords: yes for SMILES input file; no for all other
 588         file types.
 589     --overwrite
 590         Overwrite existing files.
 591     -s, --saltsMode <ByComponent, BySMARTSFile, BySMARTS>  [default: ByComponent]
 592         Specify whether to identify and remove salts based on SMARTS strings or
 593         by selecting the largest disconnected component as non-salt portion of a
 594         molecule. Possible values: ByComponent, BySMARTSFile or BySMARTS.
 595     --saltsFile <FileName or auto>  [default: auto]
 596         Specify a file name containing specification for SMARTS corresponding to salts or
 597         use default salts file, Salts.txt, available in RDKit data directory. This option is only
 598         used during 'BySMARTSFile' value of '-s, --saltsMode' option.
 599         
 600         RDKit data format: Smarts<tab>Name(optional)
 601         
 602         For example:
 603             
 604             [Cl,Br,I]
 605             [N](=O)(O)O
 606             [CH3]C(=O)O	  Acetic acid
 607             
 608     --saltsSMARTS <SMARTS text>
 609         Space delimited SMARTS specifications to use for salts identification instead
 610         their specifications in '--saltsFile'. This option is only used during 'BySMARTS'
 611         value of '-s, --saltsMode' option.
 612     -w, --workingdir <dir>
 613         Location of working directory which defaults to the current directory.
 614 
 615 Examples:
 616     To remove salts from molecules in a SMILES file by keeping largest disconnected
 617     components as non-salt portion of molecules and write out a SMILES file, type:
 618 
 619         % RDKitRemoveSalts.py -i Sample.smi -o SampleOut.smi
 620 
 621     To remove salts from molecules in a SMILES file by keeping largest disconnected
 622     components as non-salt portion of molecules, perform salt removal in multiprocessing
 623     mode on all available CPUs without loading all data into memory, and write out a
 624     SMILES file, type:
 625 
 626         % RDKitRemoveSalts.py --mp yes -i Sample.smi -o SampleOut.smi
 627 
 628     To remove salts from molecules in a SMILES file by keeping largest disconnected
 629     components as non-salt portion of molecules, perform salt removal in multiprocessing
 630     mode on all available CPUs by loading all data into memory, and write out a
 631     SMILES file, type:
 632 
 633         % RDKitRemoveSalts.py --mp yes --mpParams "inputDataMode,InMemory"
 634           -i Sample.smi -o SampleOut.smi
 635 
 636     To remove salts from molecules in a SMILES file by keeping largest disconnected
 637     components as non-salt portion of molecules, perform salt removal in multiprocessing
 638     mode on specific number of CPUs and chunk size without loading all data into memory,
 639     and write out a SMILES file, type:
 640 
 641         % RDKitRemoveSalts.py --mp yes --mpParams "inputDataMode,Lazy,
 642           numProcesses,4,chunkSize,8" -i Sample.smi -o SampleOut.smi
 643 
 644     To count number of molecules containing salts from in a SD file, using largest
 645     components as non-salt portion of molecules, without generating any output
 646     file, type:
 647 
 648         % RDKitRemoveSalts.py -m count -i Sample.sdf
 649 
 650     To remove salts from molecules in a SMILES file using SMARTS strings in default
 651     Salts.txt distributed with RDKit to identify salts and write out a SMILES file, type:
 652 
 653         % RDKitRemoveSalts.py -m remove -s BySMARTSFile -i Sample.smi
 654           -o SampleOut.smi
 655 
 656     To remove salts from molecules in a SD file using SMARTS strings in a local
 657     CustomSalts.txt to identify salts and write out a SMILES file, type:
 658 
 659         % RDKitRemoveSalts.py -m remove -s BySMARTSFile --saltsFile
 660           CustomSalts.txt -i Sample.sdf -o SampleOut.smi
 661 
 662     To remove salts from molecules in a SD file using specified SMARTS to identify
 663     salts and write out a SD file, type:
 664 
 665         % RDKitRemoveSalts.py -m remove -s BySMARTS  --saltsSMARTS
 666           '[Cl,Br,I]  [N](=O)(O)O [N](=O)(O)O'
 667           -i Sample.sdf -o SampleOut.smi
 668 
 669     To remove salts form  molecules from a CSV SMILES file, SMILES strings in column 1,
 670     name in column 2, and generate output SD file, type:
 671 
 672         % RDKitRemoveSalts.py --infileParams 
 673           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
 674           smilesNameColumn,2" --outfileParams "compute2DCoords,yes"
 675           -i SampleSMILES.csv -o SampleOut.sdf
 676 
 677 Author:
 678     Manish Sud(msud@san.rr.com)
 679 
 680 See also:
 681     RDKitConvertFileFormat.py, RDKitRemoveDuplicateMolecules.py,
 682     RDKitRemoveInvalidMolecules.py, RDKitSearchFunctionalGroups.py,
 683     RDKitSearchSMARTS.py, RDKitStandardizeMolecules.py
 684 
 685 Copyright:
 686     Copyright (C) 2026 Manish Sud. All rights reserved.
 687 
 688     The functionality available in this script is implemented using RDKit, an
 689     open source toolkit for cheminformatics developed by Greg Landrum.
 690 
 691     This file is part of MayaChemTools.
 692 
 693     MayaChemTools is free software; you can redistribute it and/or modify it under
 694     the terms of the GNU Lesser General Public License as published by the Free
 695     Software Foundation; either version 3 of the License, or (at your option) any
 696     later version.
 697 
 698 """
 699 
 700 if __name__ == "__main__":
 701     main()