       1 #!/bin/env python
       2 #
       3 # File: RDKitRemoveSalts.py
       4 # Author: Manish Sud <msud@san.rr.com>
       5 #
       6 # Copyright (C) 2018 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 # Add local python path to the global path and import standard library modules...
      32 import os
      33 import sys;  sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
      34 import time
      35 import re
      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 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 def main():
      64     """Start execution of the script"""
      65
      66     MiscUtil.PrintInfo("\n%s (RDK v%s; %s): Starting...\n" % (ScriptName, rdBase.rdkitVersion, time.asctime()))
      67
      68     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
      69
      70     # Retrieve command line arguments and options...
      71     RetrieveOptions()
      72
      73     # Process and validate command line arguments and options...
      74     ProcessOptions()
      75
      76     # Perform actions required by the script...
      77     RemoveSalts()
      78
      79     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
      80     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
      81
      82 def RemoveSalts():
      83     """Identify and remove salts from molecules"""
      84
      85     Infile = OptionsInfo["Infile"]
      86     Outfile = OptionsInfo["Outfile"]
      87
      88     CountMode = OptionsInfo["CountMode"]
      89
      90     # Setup a molecule reader...
      91     MiscUtil.PrintInfo("\nProcessing file %s..." % Infile)
      92     Mols  = RDKitUtil.ReadMolecules(Infile, **OptionsInfo["InfileParams"])
      93
      94     # Set up a molecule writer...
      95     Writer = None
      96     DuplicatesWriter = None
      97     if not CountMode:
      98         Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
      99         if Writer is None:
     100             MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
     101
     102         MiscUtil.PrintInfo("Generating file %s..." % (Outfile))
     103
     104     # Set up a salt remover...
     105     SaltsByComponentsMode = OptionsInfo["SaltsByComponentsMode"]
     106     Remover = None
     107     if not SaltsByComponentsMode:
     108         Remover = SaltRemover(defnFilename = OptionsInfo["SaltsFile"], defnData = OptionsInfo["SaltsSMARTS"], defnFormat = InputFormat.SMARTS)
     109
     110     # Process molecules...
     111     MolCount = 0
     112     ValidMolCount = 0
     113     SaltsMolCount = 0
     114
     115     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
     116
     117     for Mol in Mols:
     118         MolCount += 1
     119
     120         if Mol is None:
     121             continue
     122
     123         if RDKitUtil.IsMolEmpty(Mol):
     124             MolName = RDKitUtil.GetMolName(Mol, MolCount)
     125             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
     126             continue
     127
     128         ValidMolCount += 1
     129
     130         UnsaltedMol, SaltyStatus = RemoveMolSalts(Mol, Remover, MolCount)
     131
     132         if SaltyStatus:
     133             SaltsMolCount += 1
     134
     135         if not CountMode:
     136             if Compute2DCoords:
     137                 AllChem.Compute2DCoords(UnsaltedMol)
     138             Writer.write(UnsaltedMol)
     139
     140     if Writer is not None:
     141         Writer.close()
     142
     143     if DuplicatesWriter is not None:
     144         DuplicatesWriter.close()
     145
     146     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
     147     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
     148     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
     149
     150     MiscUtil.PrintInfo("\nNumber of molecules coontaining salts: %d" % (SaltsMolCount))
     151
     152 def RemoveMolSalts(Mol, Remover, MolCount):
     153     """Remove salts from mol and return unsalted mol along with mol salty status."""
     154
     155     UnsaltedMol = Mol
     156     SaltyStatus = False
     157
     158     if Remover is not None:
     159         KeptMol, DeletedMols = Remover.StripMolWithDeleted(Mol, dontRemoveEverything = False)
     160         if len(DeletedMols) >= 1:
     161             SaltyStatus = True
     162         if RDKitUtil.IsMolEmpty(KeptMol):
     163             if len(DeletedMols) >= 1:
     164                 # Take the larged fragment from DeletedMols
     165                 UnsaltedMol = GetLargestMol(DeletedMols)
     166     else:
     167         # Use largest fragment as unsalted molecule...
     168         MolFrags = Chem.GetMolFrags(Mol, asMols = True)
     169         if len(MolFrags) > 1:
     170             # Keep the largest fragment as unsalted molecule...
     171             SaltyStatus = True
     172             UnsaltedMol = GetLargestMol(MolFrags)
     173
     174     if SaltyStatus:
     175         Chem.SanitizeMol(UnsaltedMol)
     176         MolName = RDKitUtil.GetMolName(Mol, MolCount)
     177         if len(MolName):
     178             UnsaltedMol.SetProp("_Name", MolName)
     179
     180     return (UnsaltedMol, SaltyStatus)
     181
     182 def GetLargestMol(Mols):
     183     """Get largest mol from list of mols"""
     184
     185     LargestMol = None
     186     LargestMolSize = -1
     187     for Mol in Mols:
     188         Size = Mol.GetNumAtoms()
     189         if Size > LargestMolSize:
     190             LargestMol = Mol
     191             LargestMolSize = Size
     192
     193     return LargestMol
     194
     195 def ProcessOptions():
     196     """Process and validate command line arguments and options"""
     197
     198     MiscUtil.PrintInfo("Processing options...")
     199
     200     # Validate options...
     201     ValidateOptions()
     202
     203     OptionsInfo["Infile"] = Options["--infile"]
     204     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters("--infileParams", Options["--infileParams"], Options["--infile"])
     205
     206     OptionsInfo["Outfile"] = Options["--outfile"]
     207     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters("--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"])
     208
     209     OptionsInfo["Overwrite"] = Options["--overwrite"]
     210
     211     OptionsInfo["CountMode"] = False
     212     if re.match("^count$", Options["--mode"], re.I):
     213         OptionsInfo["CountMode"] = True
     214
     215     SaltsByComponentsMode = False
     216     SaltsBySMARTSFileMode = False
     217     SaltsBySMARTSMode = False
     218     if re.match("^ByComponent$", Options["--saltsMode"], re.I):
     219         SaltsByComponentsMode = True
     220     elif re.match("^BySMARTSFile$", Options["--saltsMode"], re.I):
     221         SaltsBySMARTSFileMode = False
     222     elif re.match("^BySMARTS$", Options["--saltsMode"], re.I):
     223         SaltsBySMARTSMode = True
     224     else:
     225         MiscUtil.PrintError("The salts mode specified, %s, using \"--saltsMode\" option is not valid." % Options["--saltsMode"])
     226     OptionsInfo["SaltsByComponentsMode"]  = SaltsByComponentsMode
     227     OptionsInfo["SaltsBySMARTSFileMode"]  = SaltsBySMARTSFileMode
     228     OptionsInfo["SaltsBySMARTSMode"]  = SaltsBySMARTSMode
     229
     230     SaltsFile = None
     231     if re.match("^BySMARTSFile$", Options["--saltsMode"], re.I):
     232         if not re.match("^auto$", Options["--saltsFile"], re.I):
     233             SaltsFile = Options["--saltsFile"]
     234     OptionsInfo["SaltsFile"] = SaltsFile
     235
     236     SaltsSMARTS = None
     237     if re.match("^BySMARTS$", Options["--saltsMode"], re.I):
     238         if not Options["--saltsSMARTS"]:
     239             MiscUtil.PrintError("No salts SMARTS pattern specified using \"--saltsSMARTS\" option during \"BySMARTS\" value of \"-s, --saltsMode\" option")
     240         SaltsSMARTS = Options["--saltsSMARTS"].strip(" ")
     241         if not len(SaltsSMARTS):
     242             MiscUtil.PrintError("Empty SMARTS pattern specified using \"--saltsSMARTS\" option during \"BySMARTS\" value of \"-s, --saltsMode\" option")
     243         if re.search(" ", SaltsSMARTS):
     244             SaltsSMARTS = re.sub('[ ]+', '\n', SaltsSMARTS)
     245
     246     OptionsInfo["SaltsSMARTS"] = SaltsSMARTS
     247
     248 def RetrieveOptions():
     249     """Retrieve command line arguments and options"""
     250
     251     # Get options...
     252     global Options
     253     Options = docopt(_docoptUsage_)
     254
     255     # Set current working directory to the specified directory...
     256     WorkingDir = Options["--workingdir"]
     257     if WorkingDir:
     258         os.chdir(WorkingDir)
     259
     260     # Handle examples option...
     261     if "--examples" in Options and Options["--examples"]:
     262         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
     263         sys.exit(0)
     264
     265 def ValidateOptions():
     266     """Validate option values"""
     267
     268     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
     269     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd smi txt csv tsv")
     270
     271     if Options["--outfile"]:
     272         MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi")
     273         MiscUtil.ValidateOptionsOutputFileOverwrite("-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"])
     274         MiscUtil.ValidateOptionsDistinctFileNames("-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"])
     275
     276     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "remove count")
     277     if re.match("^remove$", Options["--mode"], re.I):
     278         if not Options["--outfile"]:
     279             MiscUtil.PrintError("The outfile must be specified using \"-o, --outfile\" during \"remove\" value of \"-m, --mode\" option")
     280
     281     MiscUtil.ValidateOptionTextValue("--saltsMode", Options["--saltsMode"], "ByComponent BySMARTSFile BySMARTS")
     282
     283     if re.match("^BySMARTSFile$", Options["--saltsMode"], re.I):
     284         if not re.match("^auto$", Options["--saltsFile"], re.I):
     285             MiscUtil.ValidateOptionFilePath("--saltsFile", Options["--saltsFile"])
     286
     287 # Setup a usage string for docopt...
     288 _docoptUsage_ = """
     289 RDKitRemoveSalts.py - Remove salts
     290
     291 Usage:
     292     RDKitRemoveSalts.py  [--infileParams <Name,Value,...>] [--mode <remove or count>] [--outfileParams <Name,Value,...> ] [--overwrite]
     293                          [--saltsMode <ByComponent, BySMARTSFile, BySMARTS>] [--saltsFile <FileName or auto>] [--saltsSMARTS <SMARTS>]
     294                          [-w <dir>] [-o <outfile>]  -i <infile>
     295     RDKitRemoveSalts.py -h | --help | -e | --examples
     296
     297 Description:
     298     Remove salts from molecules or simply count the number of molecules containing
     299     salts. Salts are identified and removed based on either SMARTS strings or by selecting
     300     the largest disconnected components in molecules as non-salt portion of molecules.
     301
     302     The supported input file formats are: SD (.sdf, .sd), SMILES (.smi., csv, .tsv, .txt)
     303
     304     The supported output file formats are: SD (.sdf, .sd), SMILES (.smi)
     305
     306 Options:
     307     -e, --examples
     308         Print examples.
     309     -h, --help
     310         Print this help message.
     311     -i, --infile <infile>
     312         Input file name.
     313     --infileParams <Name,Value,...>  [default: auto]
     314         A comma delimited list of parameter name and value pairs for reading
     315         molecules from files. The supported parameter names for different file
     316         formats, along with their default values, are shown below:
     317
     318             SD: removeHydrogens,yes,sanitize,yes,strictParsing,yes
     319             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
     320                 smilesTitleLine,auto,sanitize,yes
     321
     322         Possible values for smilesDelimiter: space, comma or tab.
     323     -m, --mode <remove or count>  [default: remove]
     324         Specify whether to remove salts from molecules and write out molecules
     325         or or simply count the number of molecules containing salts.
     326     -o, --outfile <outfile>
     327         Output file name.
     328     --outfileParams <Name,Value,...>  [default: auto]
     329         A comma delimited list of parameter name and value pairs for writing
     330         molecules to files. The supported parameter names for different file
     331         formats, along with their default values, are shown below:
     332
     333             SD: compute2DCoords,auto,kekulize,no
     334             SMILES: kekulize,no,smilesDelimiter,space, smilesIsomeric,yes,
     335                 smilesTitleLine,yes
     336
     337         Default value for compute2DCoords: yes for SMILES input file; no for all other
     338         file types.
     339     --overwrite
     340         Overwrite existing files.
     341     -s, --saltsMode <ByComponent, BySMARTSFile, BySMARTS>  [default: ByComponent]
     342         Specify whether to identify and remove salts based on SMARTS strings or
     343         by selecting the largest disconnected component as non-salt portion of a
     344         molecule. Possible values: ByComponent, BySMARTSFile or BySMARTS.
     345     --saltsFile <FileName or auto>  [default: auto]
     346         Specify a file name containing specification for SMARTS corresponding to salts or
     347         use default salts file, Salts.txt, available in RDKit data directory. This option is only
     348         used during 'BySMARTSFile' value of '-s, --saltsMode' option.
     349
     350         RDKit data format: Smarts<tab>Name(optional)
     351
     352         For example:
     353
     354             [Cl,Br,I]
     355             [N](=O)(O)O
     356             [CH3]C(=O)O	  Acetic acid
     357
     358     --saltsSMARTS <SMARTS text>
     359         Space delimited SMARTS specifications to use for salts identification instead
     360         their specifications in '--saltsFile'. This option is only used during 'BySMARTS'
     361         value of '-s, --saltsMode' option.
     362     -w, --workingdir <dir>
     363         Location of working directory which defaults to the current directory.
     364
     365 Examples:
     366     To remove salts from molecules in a SMILES file by keeping largest disconnected
     367     components as non-salt portion of molecules and write out a SMILES file, type:
     368
     369         % RDKitRemoveSalts.py -i Sample.smi -o SampleOut.smi
     370
     371     To count number of molecule containing salts from in a SD file, using largest
     372     components as non-salt portion of molecules, without generating any output
     373     file, type:
     374
     375         % RDKitRemoveSalts.py -m count -i Sample.sdf
     376
     377     To remove salts from molecules in a SMILES file using SMARTS strings in default
     378     Salts.txt distributed with RDKit to identify salts and write out a SMILES file, type:
     379
     380         % RDKitRemoveSalts.py -m remove -s BySMARTSFile -i Sample.smi
     381           -o SampleOut.smi
     382
     383     To remove salts from molecules in a SD file using SMARTS strings in a local
     384     CustomSalts.txt to identify salts and write out a SMILES file, type:
     385
     386         % RDKitRemoveSalts.py -m remove -s BySMARTSFile --saltsFile
     387           CustomSalts.txt -i Sample.sdf -o SampleOut.smi
     388
     389     To remove salts from molecules in a SD file using specified SMARTS to identify
     390     salts and write out a SD file, type:
     391
     392         % RDKitRemoveSalts.py -m remove -s BySMARTS  --saltsSMARTS
     393           '[Cl,Br,I]  [N](=O)(O)O [N](=O)(O)O'
     394           -i Sample.sdf -o SampleOut.smi
     395
     396     To remove salts form  molecules from a CSV SMILES file, SMILES strings in column 1,
     397     name in column 2, and generate output SD file, type:
     398
     399         % RDKitRemoveSalts.py --infileParams
     400           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
     401           smilesNameColumn,2" --outfileParams "compute2DCoords,yes"
     402           -i SampleSMILES.csv -o SampleOut.sdf
     403
     404 Author:
     405     Manish Sud(msud@san.rr.com)
     406
     407 See also:
     408     RDKitConvertFileFormat.py, RDKitRemoveDuplicateMolecules.py,
     409     RDKitSearchFunctionalGroups.py, RDKitSearchSMARTS.py
     410
     411 Copyright:
     412     Copyright (C) 2018 Manish Sud. All rights reserved.
     413
     414     The functionality available in this script is implemented using RDKit, an
     415     open source toolkit for cheminformatics developed by Greg Landrum.
     416
     417     This file is part of MayaChemTools.
     418
     419     MayaChemTools is free software; you can redistribute it and/or modify it under
     420     the terms of the GNU Lesser General Public License as published by the Free
     421     Software Foundation; either version 3 of the License, or (at your option) any
     422     later version.
     423
     424 """
     425
     426 if __name__ == "__main__":
     427     main()