MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: RDKitFilterChEMBLAlerts.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 import AllChem
  42 except ImportError as ErrMsg:
  43     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
  44     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
  45     sys.exit(1)
  46 
  47 # MayaChemTools imports...
  48 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  49 try:
  50     from docopt import docopt
  51     import MiscUtil
  52     import RDKitUtil
  53 except ImportError as ErrMsg:
  54     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  55     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  56     sys.exit(1)
  57 
  58 ScriptName = os.path.basename(sys.argv[0])
  59 Options = {}
  60 OptionsInfo = {}
  61 
  62 
  63 def main():
  64     """Start execution of the script."""
  65 
  66     MiscUtil.PrintInfo(
  67         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
  68         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
  69     )
  70 
  71     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  72 
  73     # Retrieve command line arguments and options...
  74     RetrieveOptions()
  75 
  76     # Process and validate command line arguments and options...
  77     ProcessOptions()
  78 
  79     # Perform actions required by the script...
  80     PerformFiltering()
  81 
  82     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  83     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  84 
  85 
  86 def PerformFiltering():
  87     """Filter molecules using SMARTS specified in ChEMBL filters file."""
  88 
  89     # Setup ChEMBL patterns and pattern mols...
  90     MiscUtil.PrintInfo("\nSetting up ChEMBL pattern molecules for performing substructure search...")
  91     ChEMBLPatternMols = SetupChEMBLPatternMols()
  92 
  93     # Setup a molecule reader...
  94     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
  95     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
  96 
  97     # Set up molecule writers...
  98     Writer, WriterFiltered = SetupMoleculeWriters()
  99 
 100     MolCount, ValidMolCount, RemainingMolCount = ProcessMolecules(Mols, ChEMBLPatternMols, Writer, WriterFiltered)
 101 
 102     if Writer is not None:
 103         Writer.close()
 104     if WriterFiltered is not None:
 105         WriterFiltered.close()
 106 
 107     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
 108     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
 109     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
 110 
 111     MiscUtil.PrintInfo("\nNumber of remaining molecules: %d" % RemainingMolCount)
 112     MiscUtil.PrintInfo("Number of filtered molecules: %d" % (ValidMolCount - RemainingMolCount))
 113 
 114 
 115 def ProcessMolecules(Mols, ChEMBLPatternMols, Writer, WriterFiltered):
 116     """Process and filter molecules."""
 117 
 118     if OptionsInfo["MPMode"]:
 119         return ProcessMoleculesUsingMultipleProcesses(Mols, ChEMBLPatternMols, Writer, WriterFiltered)
 120     else:
 121         return ProcessMoleculesUsingSingleProcess(Mols, ChEMBLPatternMols, Writer, WriterFiltered)
 122 
 123 
 124 def ProcessMoleculesUsingSingleProcess(Mols, ChEMBLPatternMols, Writer, WriterFiltered):
 125     """Process and filter molecules using a single process."""
 126 
 127     NegateMatch = OptionsInfo["NegateMatch"]
 128     OutfileFilteredMode = OptionsInfo["OutfileFilteredMode"]
 129     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 130     SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"]
 131 
 132     MiscUtil.PrintInfo("\nFiltering molecules...")
 133 
 134     (MolCount, ValidMolCount, RemainingMolCount) = [0] * 3
 135     FirstMol = True
 136     for Mol in Mols:
 137         MolCount += 1
 138 
 139         if Mol is None:
 140             continue
 141 
 142         if RDKitUtil.IsMolEmpty(Mol):
 143             MolName = RDKitUtil.GetMolName(Mol, MolCount)
 144             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 145             continue
 146 
 147         ValidMolCount += 1
 148         if FirstMol:
 149             FirstMol = False
 150             if SetSMILESMolProps:
 151                 SetupSMILESMoleculeWritersProps(Writer, WriterFiltered, Mol)
 152 
 153         MolMatched, AlertsInfo = DoesMoleculeContainsChEMBLPattern(Mol, ChEMBLPatternMols)
 154         if MolMatched == NegateMatch:
 155             RemainingMolCount += 1
 156             WriteMolecule(Writer, Mol, AlertsInfo, Compute2DCoords)
 157         else:
 158             if OutfileFilteredMode:
 159                 WriteMolecule(WriterFiltered, Mol, AlertsInfo, Compute2DCoords)
 160 
 161     return (MolCount, ValidMolCount, RemainingMolCount)
 162 
 163 
 164 def ProcessMoleculesUsingMultipleProcesses(Mols, ChEMBLPatternMols, Writer, WriterFiltered):
 165     """Process and filter molecules using multiprocessing."""
 166 
 167     MiscUtil.PrintInfo("\nFiltering molecules using multiprocessing...")
 168 
 169     MPParams = OptionsInfo["MPParams"]
 170     NegateMatch = OptionsInfo["NegateMatch"]
 171     OutfileFilteredMode = OptionsInfo["OutfileFilteredMode"]
 172     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 173     SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"]
 174 
 175     # Setup data for initializing a worker process...
 176     MiscUtil.PrintInfo("Encoding options info and ChEMBL alert pattern molecules...")
 177     OptionsInfo["EncodedChEMBLPatternMols"] = [
 178         RDKitUtil.MolToBase64EncodedMolString(PatternMol) for PatternMol in ChEMBLPatternMols
 179     ]
 180     InitializeWorkerProcessArgs = (
 181         MiscUtil.ObjectToBase64EncodedString(Options),
 182         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
 183     )
 184 
 185     # Setup a encoded mols data iterable for a worker process...
 186     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
 187 
 188     # Setup process pool along with data initialization for each process...
 189     MiscUtil.PrintInfo(
 190         "\nConfiguring multiprocessing using %s method..."
 191         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
 192     )
 193     MiscUtil.PrintInfo(
 194         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
 195         % (
 196             MPParams["NumProcesses"],
 197             MPParams["InputDataMode"],
 198             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
 199         )
 200     )
 201 
 202     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
 203 
 204     # Start processing...
 205     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
 206         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
 207     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
 208         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
 209     else:
 210         MiscUtil.PrintError(
 211             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
 212         )
 213 
 214     (MolCount, ValidMolCount, RemainingMolCount) = [0] * 3
 215     FirstMol = True
 216     for Result in Results:
 217         MolCount += 1
 218         MolIndex, EncodedMol, MolMatched, AlertsInfo = Result
 219 
 220         if EncodedMol is None:
 221             continue
 222         ValidMolCount += 1
 223 
 224         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
 225 
 226         if FirstMol:
 227             FirstMol = False
 228             if SetSMILESMolProps:
 229                 SetupSMILESMoleculeWritersProps(Writer, WriterFiltered, Mol)
 230 
 231         if MolMatched == NegateMatch:
 232             RemainingMolCount += 1
 233             WriteMolecule(Writer, Mol, AlertsInfo, Compute2DCoords)
 234         else:
 235             if OutfileFilteredMode:
 236                 WriteMolecule(WriterFiltered, Mol, AlertsInfo, Compute2DCoords)
 237 
 238     return (MolCount, ValidMolCount, RemainingMolCount)
 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     # Decode ChEMBLPatternMols...
 253     OptionsInfo["ChEMBLPatternMols"] = [
 254         RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) for EncodedMol in OptionsInfo["EncodedChEMBLPatternMols"]
 255     ]
 256 
 257 
 258 def WorkerProcess(EncodedMolInfo):
 259     """Process data for a worker process."""
 260 
 261     MolIndex, EncodedMol = EncodedMolInfo
 262 
 263     if EncodedMol is None:
 264         return [MolIndex, None, False, None]
 265 
 266     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
 267     if RDKitUtil.IsMolEmpty(Mol):
 268         MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
 269         MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 270         return [MolIndex, None, False, None]
 271 
 272     MolMatched, AlertsInfo = DoesMoleculeContainsChEMBLPattern(Mol, OptionsInfo["ChEMBLPatternMols"])
 273 
 274     return [MolIndex, EncodedMol, MolMatched, AlertsInfo]
 275 
 276 
 277 def WriteMolecule(Writer, Mol, AlertsInfo, Compute2DCoords):
 278     """Write out molecule."""
 279 
 280     if OptionsInfo["CountMode"]:
 281         return
 282 
 283     if Compute2DCoords:
 284         AllChem.Compute2DCoords(Mol)
 285 
 286     if AlertsInfo is not None and len(AlertsInfo):
 287         AlertsCount = "%s" % len(AlertsInfo)
 288         Alerts = "; ".join(AlertsInfo)
 289         if OptionsInfo["WriteAlertsCount"]:
 290             Mol.SetProp(OptionsInfo["AlertsCountLabel"], AlertsCount)
 291         Mol.SetProp(OptionsInfo["AlertsLabel"], Alerts)
 292 
 293     Writer.write(Mol)
 294 
 295 
 296 def SetupMoleculeWriters():
 297     """Setup molecule writers."""
 298 
 299     Writer = None
 300     WriterFiltered = None
 301 
 302     if OptionsInfo["CountMode"]:
 303         return (Writer, WriterFiltered)
 304 
 305     Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
 306     if Writer is None:
 307         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
 308     MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"])
 309 
 310     if OptionsInfo["OutfileFilteredMode"]:
 311         WriterFiltered = RDKitUtil.MoleculesWriter(OptionsInfo["OutfileFiltered"], **OptionsInfo["OutfileParams"])
 312         if WriterFiltered is None:
 313             MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["OutfileFiltered"])
 314         MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["OutfileFiltered"])
 315 
 316     return (Writer, WriterFiltered)
 317 
 318 
 319 def SetupSMILESMoleculeWritersProps(Writer, WriterFiltered, Mol):
 320     """Setup properties to write for SMILES molecule writers."""
 321 
 322     if not OptionsInfo["OutfileParams"]["SetSMILESMolProps"]:
 323         return
 324 
 325     NegateMatch = OptionsInfo["NegateMatch"]
 326     SetSMILESMolAlertsProp = OptionsInfo["SetSMILESMolAlertsProp"]
 327     SMILESMolAlertsPropList = OptionsInfo["SMILESMolAlertsPropList"]
 328 
 329     if Writer is not None:
 330         RDKitUtil.SetWriterMolProps(Writer, Mol)
 331         if SetSMILESMolAlertsProp:
 332             if NegateMatch:
 333                 Writer.SetProps(SMILESMolAlertsPropList)
 334 
 335     if WriterFiltered is not None:
 336         RDKitUtil.SetWriterMolProps(WriterFiltered, Mol)
 337         if SetSMILESMolAlertsProp:
 338             if not NegateMatch:
 339                 WriterFiltered.SetProps(SMILESMolAlertsPropList)
 340 
 341 
 342 def DoesMoleculeContainsChEMBLPattern(Mol, ChEMBLPatternMols):
 343     """Check presence of ChEMBL alerts pattern in the molecule."""
 344 
 345     MatchAllAlerts = OptionsInfo["MatchAllAlerts"]
 346     AlertsInfo = []
 347     for PatternMol in ChEMBLPatternMols:
 348         if Mol.HasSubstructMatch(PatternMol, useChirality=True):
 349             AlertsInfo.append("%s: %s" % (PatternMol.GetProp("FilterType"), PatternMol.GetProp("FilterID")))
 350             if not MatchAllAlerts:
 351                 break
 352 
 353     if len(AlertsInfo) == 0:
 354         MolMatched = False
 355         AlertsInfo = None
 356     else:
 357         MolMatched = True
 358 
 359     return (MolMatched, AlertsInfo)
 360 
 361 
 362 def SetupChEMBLPatternMols():
 363     """Set up ChEMBL pattern mols for substructure search corresponding to alert mode"""
 364 
 365     PatternMols = []
 366     for FilterType in OptionsInfo["SpecifiedFilterTypes"]:
 367         for Index, Pattern in enumerate(OptionsInfo["ChEMBLFiltersMap"]["SMARTS"][FilterType]):
 368             ID = OptionsInfo["ChEMBLFiltersMap"]["IDs"][FilterType][Index]
 369 
 370             PatternMol = Chem.MolFromSmarts(Pattern)
 371             if PatternMol is None:
 372                 MiscUtil.PrintWarning("Failed to convert ChEMBL pattern, %s, into a molecule..." % Pattern)
 373                 continue
 374 
 375             # Setup FilterType and PattenMol as property of PatternMol
 376             PatternMol.SetProp("FilterType", FilterType)
 377             PatternMol.SetProp("FilterID", ID)
 378 
 379             PatternMols.append(PatternMol)
 380 
 381     return PatternMols
 382 
 383 
 384 def ProcessChEMBLAlertsMode():
 385     """Process specified alerts mode."""
 386 
 387     OptionsInfo["AlertsMode"] = Options["--alertsMode"]
 388 
 389     # Retrieve filetrs information...
 390     RetrieveChEMBLFiltersInfo()
 391 
 392     # Process alerts mode...
 393     OptionsInfo["SpecifiedFilterTypes"] = OptionsInfo["ChEMBLFiltersMap"]["FilterTypes"]
 394     if re.match("^All$", OptionsInfo["AlertsMode"], re.I):
 395         return
 396 
 397     AlertsMode = re.sub(" ", "", OptionsInfo["AlertsMode"])
 398     if not len(AlertsMode):
 399         MiscUtil.PrintError('The alerts mode specified using "-a, --alertsMode" option are empty.')
 400 
 401     CanonicalFilterTypesMap = {}
 402     for FilterType in OptionsInfo["ChEMBLFiltersMap"]["FilterTypes"]:
 403         CanonicalFilterTypesMap[FilterType.lower()] = FilterType
 404 
 405     SpecifiedFilterTypes = []
 406     for FilterType in AlertsMode.split(","):
 407         CanonicalFilterType = FilterType.lower()
 408         if CanonicalFilterType not in CanonicalFilterTypesMap:
 409             MiscUtil.PrintError(
 410                 'The altert mode, %s, specified using "-a, --alertsMode" is not valid. Supported alert modes: %s'
 411                 % (FilterType, ", ".join(OptionsInfo["ChEMBLFiltersMap"]["FilterTypes"]))
 412             )
 413 
 414         SpecifiedFilterTypes.append(CanonicalFilterTypesMap[CanonicalFilterType])
 415 
 416     OptionsInfo["SpecifiedFilterTypes"] = SpecifiedFilterTypes
 417 
 418 
 419 def ProcessChEMBLAlertsMatch():
 420     """Process specified alerts match."""
 421 
 422     AlertsMatch = Options["--alertsMatch"]
 423 
 424     MatchFirstAlert, MatchAllAlerts = [False] * 2
 425     if re.match("^First$", AlertsMatch, re.I):
 426         MatchFirstAlert = True
 427     elif re.match("^All$", AlertsMatch, re.I):
 428         MatchAllAlerts = True
 429     else:
 430         MiscUtil.PrintError(
 431             'The value %s, specified using "--alertsMatch" option is not valid. Supported values: First or All'
 432             % (AlertsMatch)
 433         )
 434 
 435     OptionsInfo["AlertsMatch"] = AlertsMatch
 436     OptionsInfo["MatchFirstAlert"] = MatchFirstAlert
 437     OptionsInfo["MatchAllAlerts"] = MatchAllAlerts
 438 
 439     # Setup labels for writing out alerts match information...
 440     OptionsInfo["AlertsCountLabel"] = "ChEMBLAlertsCount"
 441     OptionsInfo["AlertsLabel"] = "FirstChEMBLAlert" if MatchFirstAlert else "ChEMBLAlerts"
 442 
 443     # Write out alerts count only for match all alerts...
 444     OptionsInfo["WriteAlertsCount"] = True if MatchAllAlerts else False
 445 
 446     # Write out alerts match information to comma or tab delimited SMILES files...
 447     SMILESDelimiter = OptionsInfo["OutfileParams"]["SMILESDelimiter"]
 448     OptionsInfo["SetSMILESMolAlertsProp"] = True if re.match("^[\t,]", SMILESDelimiter, re.I) else False
 449 
 450     SMILESMolAlertsPropList = []
 451     if OptionsInfo["WriteAlertsCount"]:
 452         SMILESMolAlertsPropList.append(OptionsInfo["AlertsCountLabel"])
 453     SMILESMolAlertsPropList.append(OptionsInfo["AlertsLabel"])
 454     OptionsInfo["SMILESMolAlertsPropList"] = SMILESMolAlertsPropList
 455 
 456 
 457 def RetrieveChEMBLFiltersInfo():
 458     """Retrieve information for ChEMBL filters."""
 459 
 460     MayaChemToolsDataDir = MiscUtil.GetMayaChemToolsLibDataPath()
 461     ChEMBLFiltersFilePath = os.path.join(MayaChemToolsDataDir, "ChEMBLFilters.csv")
 462 
 463     MiscUtil.PrintInfo("\nRetrieving ChEMBL alerts SMARTS patterns from file %s" % (ChEMBLFiltersFilePath))
 464 
 465     Delimiter = ","
 466     QuoteChar = '"'
 467     IgnoreHeaderLine = True
 468     FilterLinesWords = MiscUtil.GetTextLinesWords(ChEMBLFiltersFilePath, Delimiter, QuoteChar, IgnoreHeaderLine)
 469 
 470     ChEMBLFiltersMap = {}
 471     ChEMBLFiltersMap["FilterTypes"] = []
 472     ChEMBLFiltersMap["IDs"] = {}
 473     ChEMBLFiltersMap["SMARTS"] = {}
 474 
 475     for LineWords in FilterLinesWords:
 476         FilterType = LineWords[0]
 477         ID = LineWords[1]
 478         SMARTS = LineWords[2]
 479 
 480         if FilterType not in ChEMBLFiltersMap["FilterTypes"]:
 481             ChEMBLFiltersMap["FilterTypes"].append(FilterType)
 482             ChEMBLFiltersMap["IDs"][FilterType] = []
 483             ChEMBLFiltersMap["SMARTS"][FilterType] = []
 484 
 485         ChEMBLFiltersMap["IDs"][FilterType].append(ID)
 486         ChEMBLFiltersMap["SMARTS"][FilterType].append(SMARTS)
 487 
 488     OptionsInfo["ChEMBLFiltersMap"] = ChEMBLFiltersMap
 489 
 490     MiscUtil.PrintInfo("\nTotal number alerts: %d" % len(FilterLinesWords))
 491     MiscUtil.PrintInfo(
 492         "Number of filter family types: %d\nFilter familty types: %s\n"
 493         % (len(ChEMBLFiltersMap["FilterTypes"]), ", ".join(ChEMBLFiltersMap["FilterTypes"]))
 494     )
 495 
 496     for FilterType in ChEMBLFiltersMap["FilterTypes"]:
 497         MiscUtil.PrintInfo(
 498             "Filter family type: %s; Number of alerts: %d" % (FilterType, len(ChEMBLFiltersMap["IDs"][FilterType]))
 499         )
 500     MiscUtil.PrintInfo("")
 501 
 502 
 503 def ProcessOptions():
 504     """Process and validate command line arguments and options."""
 505 
 506     MiscUtil.PrintInfo("Processing options...")
 507 
 508     # Validate options...
 509     ValidateOptions()
 510 
 511     OptionsInfo["Infile"] = Options["--infile"]
 512     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 513         "--infileParams", Options["--infileParams"], Options["--infile"]
 514     )
 515 
 516     OptionsInfo["Outfile"] = Options["--outfile"]
 517     ParamsDefaultInfoOverride = {"SMILESMolProps": True}
 518     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 519         "--outfileParams",
 520         Options["--outfileParams"],
 521         Options["--infile"],
 522         Options["--outfile"],
 523         ParamsDefaultInfo=ParamsDefaultInfoOverride,
 524     )
 525 
 526     FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"])
 527     OutfileFiltered = "%s_Filtered.%s" % (FileName, FileExt)
 528     OptionsInfo["OutfileFiltered"] = OutfileFiltered
 529     OptionsInfo["OutfileFilteredMode"] = True if re.match("^yes$", Options["--outfileFiltered"], re.I) else False
 530 
 531     OptionsInfo["Overwrite"] = Options["--overwrite"]
 532 
 533     OptionsInfo["CountMode"] = True if re.match("^count$", Options["--mode"], re.I) else False
 534     OptionsInfo["NegateMatch"] = True if re.match("^yes$", Options["--negate"], re.I) else False
 535 
 536     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 537     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 538 
 539     ProcessChEMBLAlertsMode()
 540     ProcessChEMBLAlertsMatch()
 541 
 542 
 543 def RetrieveOptions():
 544     """Retrieve command line arguments and options."""
 545 
 546     # Get options...
 547     global Options
 548     Options = docopt(_docoptUsage_)
 549 
 550     # Set current working directory to the specified directory...
 551     WorkingDir = Options["--workingdir"]
 552     if WorkingDir:
 553         os.chdir(WorkingDir)
 554 
 555     # Handle examples option...
 556     if "--examples" in Options and Options["--examples"]:
 557         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 558         sys.exit(0)
 559 
 560 
 561 def ValidateOptions():
 562     """Validate option values."""
 563 
 564     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 565     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd smi txt csv tsv")
 566 
 567     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi")
 568     if re.match("^filter$", Options["--mode"], re.I):
 569         MiscUtil.ValidateOptionsOutputFileOverwrite(
 570             "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 571         )
 572         MiscUtil.ValidateOptionsDistinctFileNames(
 573             "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 574         )
 575 
 576     MiscUtil.ValidateOptionTextValue("--alertsMatch", Options["--alertsMatch"], "First All")
 577 
 578     MiscUtil.ValidateOptionTextValue("--outfileFiltered", Options["--outfileFiltered"], "yes no")
 579 
 580     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "filter count")
 581     if re.match("^filter$", Options["--mode"], re.I):
 582         if not Options["--outfile"]:
 583             MiscUtil.PrintError(
 584                 'The outfile must be specified using "-o, --outfile" during "filter" value of "-m, --mode" option'
 585             )
 586 
 587     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 588     MiscUtil.ValidateOptionTextValue("-n, --negate", Options["--negate"], "yes no")
 589 
 590 
 591 # Setup a usage string for docopt...
 592 _docoptUsage_ = """
 593 RDKitFilterChEMBLAlterts.py - Filter ChEMBL alerts
 594 
 595 Usage:
 596     RDKitFilterChEMBLAlerts.py  [--alertsMode <All or Type,Type,...>] [--alertsMatch <First or All>]
 597                                 [--infileParams <Name,Value,...>] [--mode <filter or count>]
 598                                 [--mp <yes or no>] [--mpParams <Name,Value,...>]
 599                                 [--outfileFiltered <yes or no>] [ --outfileParams <Name,Value,...>]
 600                                 [--negate <yes or no>] [--overwrite] [-w <dir>] -i <infile> -o <outfile>
 601     RDKitFilterChEMBLAlerts.py -h | --help | -e | --examples
 602 
 603 Description:
 604     Filter molecules from an input file for ChEMBL structural alerts by performing
 605     a substructure search using SMARTS patterns specified in MAYACHEMTOOLS/
 606     lib/data/ChEMBLFilters.csv file and write out appropriate molecules to an
 607     output file or simply count the number of filtered molecules.
 608 
 609     The supported input file formats are: SD (.sdf, .sd), SMILES (.smi, .csv,
 610     .tsv, .txt)
 611 
 612     The supported output file formats are: SD (.sdf, .sd), SMILES (.smi)
 613 
 614 Options:
 615     -a, --alertsMode <All or Type, Type,...>  [default: All]
 616         All or a comma delimited list of ChEMBL filter types to use for filtering
 617         molecules. 
 618         
 619         The supported filter family types, along with a description, are show below:
 620         
 621             BMS: Bristol-Myers Squibb HTS Deck Filters
 622             Dundee: University of Dundee NTD Screening Library Filters
 623             Glaxo: Bristol-Myers Squibb HTS Deck Filters
 624             Inpharmatica
 625             MLSMR: NIH MLSMR Excluded Functionality Filters
 626             PfizerLINT: Pfizer LINT filters
 627             SureChEMBL
 628         
 629     --alertsMatch <First or All>  [default: First]
 630         Stop after matching only first alert or match all ChEMBL alerts for
 631         filtering molecules.
 632         
 633         The 'ChEMBLAlertsCount' and 'ChEMBLAlerts' data fields are added to
 634         SD file containing filtered molecules for 'All' value of '-altersMatch'. In
 635         addition, these data fields are only written to tab or comma delimited
 636         SMILES file.
 637         
 638         Format:
 639             
 640             > <ChEMBLAlertsCount>
 641             Number
 642             
 643             > <ChEMBLAlerts>
 644             FilterType: ID; FilterType: ID... ... ...``
 645             
 646     -e, --examples
 647         Print examples.
 648     -h, --help
 649         Print this help message.
 650     -i, --infile <infile>
 651         Input file name.
 652     --infileParams <Name,Value,...>  [default: auto]
 653         A comma delimited list of parameter name and value pairs for reading
 654         molecules from files. The supported parameter names for different file
 655         formats, along with their default values, are shown below:
 656             
 657             SD: removeHydrogens,yes,sanitize,yes,strictParsing,yes
 658             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 659                 smilesTitleLine,auto,sanitize,yes
 660             
 661         Possible values for smilesDelimiter: space, comma or tab.
 662     -m, --mode <filter or count>  [default: filter]
 663         Specify whether to filter the matched molecules and write out the rest of the 
 664         molecules to an outfile or simply count the number of matched molecules
 665         marked for filtering.
 666     --mp <yes or no>  [default: no]
 667         Use multiprocessing.
 668          
 669         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 670         function employing lazy RDKit data iterable. This allows processing of
 671         arbitrary large data sets without any additional requirements memory.
 672         
 673         All input data may be optionally loaded into memory by mp.Pool.map()
 674         before starting worker processes in a process pool by setting the value
 675         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 676         
 677         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 678         data mode may adversely impact the performance. The '--mpParams' section
 679         provides additional information to tune the value of 'chunkSize'.
 680     --mpParams <Name,Value,...>  [default: auto]
 681         A comma delimited list of parameter name and value pairs to configure
 682         multiprocessing.
 683         
 684         The supported parameter names along with their default and possible
 685         values are shown below:
 686         
 687             chunkSize, auto
 688             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 689             numProcesses, auto   [ Default: mp.cpu_count() ]
 690         
 691         These parameters are used by the following functions to configure and
 692         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 693         mp.Pool.imap().
 694         
 695         The chunkSize determines chunks of input data passed to each worker
 696         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 697         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 698         
 699         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 700         automatically converts RDKit data iterable into a list, loads all data into
 701         memory, and calculates the default chunkSize using the following method
 702         as shown in its code:
 703         
 704             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 705             if extra: chunkSize += 1
 706         
 707         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 708         and 100 data items.
 709         
 710         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 711         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 712         data into memory. Consequently, the size of input data is not known a priori.
 713         It's not possible to estimate an optimal value for the chunkSize. The default 
 714         chunkSize is set to 1.
 715         
 716         The default value for the chunkSize during 'Lazy' data mode may adversely
 717         impact the performance due to the overhead associated with exchanging
 718         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 719         a larger value during 'Lazy' input data mode, based on the size of your input
 720         data and number of processes in the process pool.
 721         
 722         The mp.Pool.map() function waits for all worker processes to process all
 723         the data and return the results. The mp.Pool.imap() function, however,
 724         returns the the results obtained from worker processes as soon as the
 725         results become available for specified chunks of data.
 726         
 727         The order of data in the results returned by both mp.Pool.map() and 
 728         mp.Pool.imap() functions always corresponds to the input data.
 729     -n, --negate <yes or no>  [default: no]
 730         Specify whether to filter molecules not matching the ChEMBL filters specified by
 731         SMARTS patterns.
 732     -o, --outfile <outfile>
 733         Output file name.
 734     --outfileFiltered <yes or no>  [default: no]
 735         Write out a file containing filtered molecules. Its name is automatically
 736         generated from the specified output file. Default: <OutfileRoot>_
 737         Filtered.<OutfileExt>.
 738     --outfileParams <Name,Value,...>  [default: auto]
 739         A comma delimited list of parameter name and value pairs for writing
 740         molecules to files. The supported parameter names for different file
 741         formats, along with their default values, are shown below:
 742             
 743             SD: compute2DCoords,auto,kekulize,yes,forceV3000,no
 744             SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes,
 745                 smilesTitleLine,yes,smilesMolName,yes,smilesMolProps,yes
 746             
 747         Default value for compute2DCoords: yes for SMILES input file; no for all other
 748         file types.
 749     --overwrite
 750         Overwrite existing files.
 751     -w, --workingdir <dir>
 752         Location of working directory which defaults to the current directory.
 753 
 754 Examples:
 755     To count the number of molecules not containing any substructure corresponding
 756     to any ChEMBL SMARTS patterns and write out SMILES files containing these molecules,
 757     type: 
 758 
 759         % RDKitFilterChEMBLAlerts.py -i Sample.smi -o SampleOut.smi
 760 
 761     To count the number of molecules not containing any substructure corresponding
 762     to any ChEMBL SMARTS patterns and write out comma delmited SMILES files
 763     containing these and filtered molecules along with the alerts information for
 764     filtered molecules matching first pattern, type: 
 765 
 766         % RDKitFilterChEMBLAlerts.py --outfileFiltered yes --outfileParams
 767           "SMILESDelimiter,comma" -i Sample.smi -o SampleOut.smi
 768 
 769     To count the number of molecules not containing any substructure corresponding
 770     to any ChEMBL SMARTS patterns and write out comma delmited SMILES files
 771     containing these and filtered molecules along with the alerts information for
 772     filtered molecules matching all patterns, type: 
 773 
 774         % RDKitFilterChEMBLAlerts.py --alertsMatch All --outfileFiltered yes
 775           --outfileParams "SMILESDelimiter,comma" -i Sample.smi
 776           -o SampleOut.smi
 777 
 778     To count the number of molecules not containing any substructure corresponding
 779     to any ChEMBL SMARTS patterns and write out SD files containing these and filtered
 780     molecules along with the alerts information for filtered molecules matching all
 781     patterns, type: 
 782 
 783         % RDKitFilterChEMBLAlerts.py --alertsMatch All --outfileFiltered yes
 784           -i Sample.smi -o SampleOut.sdf
 785 
 786     To count the number of molecules not containing any substructure corresponding to
 787     ChEMBL SMARTS patterns, perform filtering in multiprocessing mode on all
 788     available CPUs without loading all data into memory, and write out a SMILES file, type: 
 789 
 790         % RDKitFilterChEMBLAlerts.py --mp yes -i Sample.smi -o SampleOut.smi
 791 
 792     To count the number of molecules not containing any substructure corresponding to
 793     ChEMBL SMARTS patterns, perform filtering in multiprocessing mode on all
 794     available CPUs by loading all data into memory, and write out a SD file, type: 
 795 
 796         % RDKitFilterChEMBLAlerts.py --mp yes --mpParams "inputDataMode,
 797           InMemory" -i Sample.smi -o SampleOut.sdf
 798 
 799     To count the number of molecules not containing any substructure corresponding to
 800     ChEMBL SMARTS patterns, perform filtering in multiprocessing mode on specific
 801     number of CPUs and chunk size without loading all data into memory, and
 802     write out a SD file, type: 
 803 
 804         % RDKitFilterChEMBLAlerts.py --mp yes --mpParams "inputDataMode,Lazy,
 805           numProcesses,4,chunkSize,8" -i Sample.smi -o SampleOut.sdf
 806 
 807     To only count the number of molecules not containing any substructure corresponding
 808     to BMS ChEMBL SMARTS patterns without writing out any files, type: 
 809 
 810         % RDKitFilterChEMBLAlerts.py -m count -a BMS -i Sample.sdf
 811           -o SampleOut.smi
 812 
 813     To count the number of molecules not containing any substructure corresponding
 814     to Pfizer LINT ChEMBL SMARTS patterns in a  CSV SMILES file and write out a SD file,
 815     type:  
 816 
 817         % RDKitFilterChEMBLAlerts.py --altertsMode PfizerLINT --infileParams
 818           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
 819           smilesNameColumn,2" --outfileParams "compute2DCoords,yes"
 820           -i SampleSMILES.csv -o SampleOut.sdf
 821 
 822 Author:
 823     Manish Sud(msud@san.rr.com)
 824 
 825 See also:
 826     RDKitFilterPAINS.py, RDKitConvertFileFormat.py, RDKitSearchSMARTS.py
 827 
 828 Copyright:
 829     Copyright (C) 2026 Manish Sud. All rights reserved.
 830 
 831     The functionality available in this script is implemented using RDKit, an
 832     open source toolkit for cheminformatics developed by Greg Landrum.
 833 
 834     This file is part of MayaChemTools.
 835 
 836     MayaChemTools is free software; you can redistribute it and/or modify it under
 837     the terms of the GNU Lesser General Public License as published by the Free
 838     Software Foundation; either version 3 of the License, or (at your option) any
 839     later version.
 840 
 841 """
 842 
 843 if __name__ == "__main__":
 844     main()