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