1 #!/bin/env python 2 # 3 # File: RDKitSearchFunctionalGroups.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 from rdkit.Chem import FunctionalGroups 43 except ImportError as ErrMsg: 44 sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg) 45 sys.stderr.write("Check/update your RDKit environment and try again.\n\n") 46 sys.exit(1) 47 48 # MayaChemTools imports... 49 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python")) 50 try: 51 from docopt import docopt 52 import MiscUtil 53 import RDKitUtil 54 except ImportError as ErrMsg: 55 sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg) 56 sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n") 57 sys.exit(1) 58 59 ScriptName = os.path.basename(sys.argv[0]) 60 Options = {} 61 OptionsInfo = {} 62 63 FunctionalGroupsMap = {} 64 65 66 def main(): 67 """Start execution of the script.""" 68 69 MiscUtil.PrintInfo( 70 "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n" 71 % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime()) 72 ) 73 74 (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime() 75 76 # Retrieve command line arguments and options... 77 RetrieveOptions() 78 79 # Process and validate command line arguments and options... 80 ProcessOptions() 81 82 # Perform actions required by the script... 83 PerformFunctionalGroupsSearch() 84 85 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName) 86 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime)) 87 88 89 def PerformFunctionalGroupsSearch(): 90 """Retrieve functional groups information and perform search.""" 91 92 # Process functional groups info... 93 ProcessFunctionalGroupsInfo() 94 95 # Setup pattern mols for functional group SMARTS... 96 GroupsPatternMols = SetupFunctionalGroupsSMARTSPatterns() 97 98 # Setup a molecule reader... 99 MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"]) 100 Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"]) 101 102 # Set up molecule writers... 103 Writer, GroupOutfilesWriters = SetupMoleculeWriters() 104 105 MolCount, ValidMolCount, RemainingMolCount, GroupsPatternMatchCountList = ProcessMolecules( 106 Mols, GroupsPatternMols, Writer, GroupOutfilesWriters 107 ) 108 109 if Writer is not None: 110 Writer.close() 111 for GroupOutfileWriter in GroupOutfilesWriters: 112 GroupOutfileWriter.close() 113 114 MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount) 115 MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount) 116 MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount)) 117 118 MiscUtil.PrintInfo("\nTotal number of molecules matched against specified match criteria: %d" % RemainingMolCount) 119 120 MiscUtil.PrintInfo("\nNumber of molecuels matched against individual functional groups:") 121 MiscUtil.PrintInfo("FunctionalGroupName,MatchCount") 122 123 for GroupIndex in range(0, len(OptionsInfo["SpecifiedFunctionalGroups"])): 124 GroupName = OptionsInfo["SpecifiedFunctionalGroups"][GroupIndex] 125 if OptionsInfo["SpecifiedFunctionalGroupsNegateMatch"][GroupIndex]: 126 GroupName = "!" + GroupName 127 GroupMatchCount = GroupsPatternMatchCountList[GroupIndex] 128 MiscUtil.PrintInfo("%s,%d" % (GroupName, GroupMatchCount)) 129 130 131 def ProcessMolecules(Mols, GroupsPatternMols, Writer, GroupOutfilesWriters): 132 """Process and search molecules.""" 133 134 if OptionsInfo["MPMode"]: 135 return ProcessMoleculesUsingMultipleProcesses(Mols, GroupsPatternMols, Writer, GroupOutfilesWriters) 136 else: 137 return ProcessMoleculesUsingSingleProcess(Mols, GroupsPatternMols, Writer, GroupOutfilesWriters) 138 139 140 def ProcessMoleculesUsingSingleProcess(Mols, GroupsPatternMols, Writer, GroupOutfilesWriters): 141 """Process and search molecules using a single process.""" 142 143 MiscUtil.PrintInfo("\nSearching functional groups...") 144 145 Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"] 146 CombineMatchResults = OptionsInfo["CombineMatchResults"] 147 SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"] 148 149 GroupsPatternsMatchCountList = [0] * len(OptionsInfo["SpecifiedFunctionalGroups"]) 150 (MolCount, ValidMolCount, RemainingMolCount) = [0] * 3 151 152 FirstMol = True 153 for Mol in Mols: 154 MolCount += 1 155 156 if Mol is None: 157 continue 158 159 if RDKitUtil.IsMolEmpty(Mol): 160 MolName = RDKitUtil.GetMolName(Mol, MolCount) 161 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 162 continue 163 164 ValidMolCount += 1 165 if FirstMol: 166 FirstMol = False 167 if SetSMILESMolProps: 168 if Writer is not None: 169 RDKitUtil.SetWriterMolProps(Writer, Mol) 170 for GroupOutfileWriter in GroupOutfilesWriters: 171 if GroupOutfileWriter is not None: 172 RDKitUtil.SetWriterMolProps(GroupOutfileWriter, Mol) 173 174 # Match molecule against functional group patterns... 175 MolMatched, GroupsPatternMatchStatusList = MatchMolecule(Mol, GroupsPatternMols) 176 177 # Update functional group match count... 178 for GroupIndex, MatchStatus in enumerate(GroupsPatternMatchStatusList): 179 if MatchStatus: 180 GroupsPatternsMatchCountList[GroupIndex] += 1 181 182 if not MolMatched: 183 continue 184 185 RemainingMolCount += 1 186 WriteMolecule( 187 Writer, GroupOutfilesWriters, Mol, Compute2DCoords, CombineMatchResults, GroupsPatternMatchStatusList 188 ) 189 190 return (MolCount, ValidMolCount, RemainingMolCount, GroupsPatternsMatchCountList) 191 192 193 def ProcessMoleculesUsingMultipleProcesses(Mols, GroupsPatternMols, Writer, GroupOutfilesWriters): 194 """Process and search molecules using multiprocessing.""" 195 196 MiscUtil.PrintInfo("\nSearching functional groups using multiprocessing...") 197 198 MPParams = OptionsInfo["MPParams"] 199 Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"] 200 CombineMatchResults = OptionsInfo["CombineMatchResults"] 201 SetSMILESMolProps = OptionsInfo["OutfileParams"]["SetSMILESMolProps"] 202 203 # Setup data for initializing a worker process... 204 MiscUtil.PrintInfo("Encoding options info and functional groups pattern molecules...") 205 OptionsInfo["EncodedGroupPatternMols"] = [ 206 RDKitUtil.MolToBase64EncodedMolString(PatternMol) for PatternMol in GroupsPatternMols 207 ] 208 InitializeWorkerProcessArgs = ( 209 MiscUtil.ObjectToBase64EncodedString(Options), 210 MiscUtil.ObjectToBase64EncodedString(OptionsInfo), 211 MiscUtil.ObjectToBase64EncodedString(FunctionalGroupsMap), 212 ) 213 214 # Setup a encoded mols data iterable for a worker process... 215 WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols) 216 217 # Setup process pool along with data initialization for each process... 218 MiscUtil.PrintInfo( 219 "\nConfiguring multiprocessing using %s method..." 220 % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()") 221 ) 222 MiscUtil.PrintInfo( 223 "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n" 224 % ( 225 MPParams["NumProcesses"], 226 MPParams["InputDataMode"], 227 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]), 228 ) 229 ) 230 231 ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs) 232 233 # Start processing... 234 if re.match("^Lazy$", MPParams["InputDataMode"], re.I): 235 Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 236 elif re.match("^InMemory$", MPParams["InputDataMode"], re.I): 237 Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 238 else: 239 MiscUtil.PrintError( 240 'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"]) 241 ) 242 243 GroupsPatternsMatchCountList = [0] * len(OptionsInfo["SpecifiedFunctionalGroups"]) 244 245 (MolCount, ValidMolCount, RemainingMolCount) = [0] * 3 246 247 FirstMol = True 248 for Result in Results: 249 MolCount += 1 250 MolIndex, EncodedMol, MolMatched, GroupsPatternMatchStatusList = Result 251 252 if EncodedMol is None: 253 continue 254 ValidMolCount += 1 255 256 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 257 258 if FirstMol: 259 FirstMol = False 260 if SetSMILESMolProps: 261 if Writer is not None: 262 RDKitUtil.SetWriterMolProps(Writer, Mol) 263 for GroupOutfileWriter in GroupOutfilesWriters: 264 if GroupOutfileWriter is not None: 265 RDKitUtil.SetWriterMolProps(GroupOutfileWriter, Mol) 266 267 # Update functional group match count... 268 for GroupIndex, MatchStatus in enumerate(GroupsPatternMatchStatusList): 269 if MatchStatus: 270 GroupsPatternsMatchCountList[GroupIndex] += 1 271 272 if not MolMatched: 273 continue 274 275 RemainingMolCount += 1 276 WriteMolecule( 277 Writer, GroupOutfilesWriters, Mol, Compute2DCoords, CombineMatchResults, GroupsPatternMatchStatusList 278 ) 279 280 return (MolCount, ValidMolCount, RemainingMolCount, GroupsPatternsMatchCountList) 281 282 283 def InitializeWorkerProcess(*EncodedArgs): 284 """Initialize data for a worker process.""" 285 286 global Options, OptionsInfo, FunctionalGroupsMap 287 288 MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid()) 289 290 # Decode Options and OptionInfo... 291 Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0]) 292 OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1]) 293 FunctionalGroupsMap = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[2]) 294 295 # Decode ChEMBLPatternMols... 296 OptionsInfo["GroupPatternMols"] = [ 297 RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) for EncodedMol in OptionsInfo["EncodedGroupPatternMols"] 298 ] 299 300 301 def WorkerProcess(EncodedMolInfo): 302 """Process data for a worker process.""" 303 304 MolIndex, EncodedMol = EncodedMolInfo 305 306 if EncodedMol is None: 307 return [MolIndex, None, False, None] 308 309 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 310 if RDKitUtil.IsMolEmpty(Mol): 311 MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1)) 312 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 313 return [MolIndex, None, False, None] 314 315 # Match molecule against functional group patterns... 316 MolMatched, GroupsPatternMatchStatusList = MatchMolecule(Mol, OptionsInfo["GroupPatternMols"]) 317 318 return [MolIndex, EncodedMol, MolMatched, GroupsPatternMatchStatusList] 319 320 321 def MatchMolecule(Mol, GroupsPatternMols): 322 """Search for functional groups in a molecule.""" 323 324 GroupsPatternMatchStatusList = [] 325 326 # Match pattern mols... 327 for GroupIndex in range(0, len(OptionsInfo["SpecifiedFunctionalGroups"])): 328 Status = DoesPatternMolMatch( 329 GroupsPatternMols[GroupIndex], 330 Mol, 331 OptionsInfo["UseChirality"], 332 OptionsInfo["SpecifiedFunctionalGroupsNegateMatch"][GroupIndex], 333 ) 334 GroupsPatternMatchStatusList.append(Status) 335 336 # Match mol against all specified criteria... 337 MolMatched = DoesMolMeetSpecifiedMatchCriteria( 338 GroupsPatternMatchStatusList, OptionsInfo["CombineMatchResults"], OptionsInfo["AndCombineOperatorMode"] 339 ) 340 341 return (MolMatched, GroupsPatternMatchStatusList) 342 343 344 def DoesMolMeetSpecifiedMatchCriteria(GroupsPatternMolsMatchStatus, CombineMatchResults, AndCombineOperatorMode): 345 """Match molecule using specified match criteia.""" 346 347 if CombineMatchResults and AndCombineOperatorMode: 348 # Must match all specified SMARTS 349 Status = True 350 for MatchStatus in GroupsPatternMolsMatchStatus: 351 if not MatchStatus: 352 Status = False 353 break 354 else: 355 # One match is enough... 356 Status = False 357 for MatchStatus in GroupsPatternMolsMatchStatus: 358 if MatchStatus: 359 Status = True 360 break 361 362 return Status 363 364 365 def WriteMolecule( 366 Writer, GroupOutfilesWriters, Mol, Compute2DCoords, CombineMatchResults, GroupsPatternMatchStatusList 367 ): 368 """Write out molecule.""" 369 370 if OptionsInfo["CountMode"]: 371 return 372 373 if Compute2DCoords: 374 AllChem.Compute2DCoords(Mol) 375 376 if CombineMatchResults: 377 Writer.write(Mol) 378 else: 379 for GroupIndex in range(0, len(GroupsPatternMatchStatusList)): 380 if GroupsPatternMatchStatusList[GroupIndex]: 381 GroupOutfilesWriters[GroupIndex].write(Mol) 382 383 384 def SetupMoleculeWriters(): 385 """Set up molecule writers for output files.""" 386 387 Writer = None 388 GroupOutfilesWriters = [] 389 390 if OptionsInfo["CountMode"]: 391 return (Writer, GroupOutfilesWriters) 392 393 Outfile = OptionsInfo["Outfile"] 394 CombineMatchResults = OptionsInfo["CombineMatchResults"] 395 GroupsOutfiles = OptionsInfo["SpecifiedFunctionalGroupsOutfiles"] 396 397 if CombineMatchResults: 398 Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"]) 399 if Writer is None: 400 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile) 401 MiscUtil.PrintInfo("Generating file %s..." % Outfile) 402 else: 403 for GroupOutfile in GroupsOutfiles: 404 GroupOutfileWriter = RDKitUtil.MoleculesWriter(GroupOutfile, **OptionsInfo["OutfileParams"]) 405 if GroupOutfileWriter is None: 406 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Writer) 407 GroupOutfilesWriters.append(GroupOutfileWriter) 408 409 GroupsCount = len(GroupsOutfiles) 410 if GroupsCount > 4: 411 MiscUtil.PrintInfo( 412 "Generating %d output files with the following file name format: %s<GroupName>.%s" 413 % (GroupsCount, OptionsInfo["OutfileBasename"], OptionsInfo["OutfileExt"]) 414 ) 415 else: 416 Delmiter = ", " 417 OutfileNames = Delmiter.join(GroupsOutfiles) 418 MiscUtil.PrintInfo("Generating %d output files: %s..." % (GroupsCount, OutfileNames)) 419 420 return (Writer, GroupOutfilesWriters) 421 422 423 def DoesPatternMolMatch(PatternMol, Mol, UseChirality, NegateMatch): 424 """Perform a substructure match for the presence of pattern molecule in a molecule.""" 425 426 MolMatched = Mol.HasSubstructMatch(PatternMol, useChirality=UseChirality) 427 if NegateMatch: 428 if MolMatched: 429 MolMatched = False 430 else: 431 MolMatched = True 432 433 return MolMatched 434 435 436 def ProcessFunctionalGroupsInfo(): 437 """Process functional groups information.""" 438 439 RetrieveFunctionalGroupsInfo() 440 ProcessSpecifiedFunctionalGroups() 441 442 SetupFunctionalGroupsOutputFileNames() 443 444 445 def ProcessSpecifiedFunctionalGroups(): 446 """Process and validate specified functional groups.""" 447 448 OptionsInfo["SpecifiedFunctionalGroups"] = [] 449 OptionsInfo["SpecifiedFunctionalGroupsNegateMatch"] = [] 450 451 if re.match("^All$", OptionsInfo["FunctionalGroups"], re.I): 452 OptionsInfo["SpecifiedFunctionalGroups"] = FunctionalGroupsMap["Names"] 453 OptionsInfo["SpecifiedFunctionalGroupsNegateMatch"] = [False] * len(OptionsInfo["SpecifiedFunctionalGroups"]) 454 return 455 456 # Set up a map of valid group names for checking specified group names... 457 CanonicalGroupNameMap = {} 458 for GroupName in FunctionalGroupsMap["Names"]: 459 CanonicalGroupNameMap[GroupName.lower()] = GroupName 460 461 # Parse and validate specified names... 462 GroupNames = re.sub(" ", "", OptionsInfo["FunctionalGroups"]) 463 if not GroupNames: 464 MiscUtil.PrintError('No functional group name specified for "-f, --functionalGroups" option') 465 466 SpecifiedFunctionalGroups = [] 467 SpecifiedNegateMatchStatus = [] 468 469 for GroupName in GroupNames.split(","): 470 CanonicalGroupName = GroupName.lower() 471 NegateMatchStatus = False 472 if re.match("^!", CanonicalGroupName, re.I): 473 NegateMatchStatus = True 474 CanonicalGroupName = re.sub("^!", "", CanonicalGroupName) 475 if CanonicalGroupName in CanonicalGroupNameMap: 476 SpecifiedFunctionalGroups.append(CanonicalGroupNameMap[CanonicalGroupName]) 477 SpecifiedNegateMatchStatus.append(NegateMatchStatus) 478 else: 479 MiscUtil.PrintWarning( 480 'The functional group name, %s, specified using "-f, --functionalGroups" option is not a valid name.' 481 % (GroupName) 482 ) 483 484 if not len(SpecifiedFunctionalGroups): 485 MiscUtil.PrintError('No valid functional group names specified for "-f, --functionalGroups" option') 486 487 OptionsInfo["SpecifiedFunctionalGroups"] = SpecifiedFunctionalGroups 488 OptionsInfo["SpecifiedFunctionalGroupsNegateMatch"] = SpecifiedNegateMatchStatus 489 490 491 def SetupFunctionalGroupsSMARTSPatterns(): 492 """Setup SMARTS patterns for specified functional groups.""" 493 494 OptionsInfo["SpecifiedFunctionalGroupsSMARTSPatterns"] = [] 495 FunctionalGroupsPatternMols = [] 496 497 for Name in OptionsInfo["SpecifiedFunctionalGroups"]: 498 SMARTSPattern = FunctionalGroupsMap["SMARTSPattern"][Name] 499 PatternMol = Chem.MolFromSmarts(SMARTSPattern) 500 if PatternMol is None: 501 MiscUtil.PrintError("Failed to parse SMARTS pattern, %s, for function group, %s" % (SMARTSPattern, Name)) 502 503 OptionsInfo["SpecifiedFunctionalGroupsSMARTSPatterns"].append(SMARTSPattern) 504 FunctionalGroupsPatternMols.append(PatternMol) 505 506 return FunctionalGroupsPatternMols 507 508 509 def SetupFunctionalGroupsOutputFileNames(): 510 """Setup output file names for specified functional group names.""" 511 512 OptionsInfo["SpecifiedFunctionalGroupsOutfiles"] = [] 513 514 if OptionsInfo["CountMode"]: 515 # No need of any output file... 516 return 517 518 if OptionsInfo["CombineMatchResults"]: 519 # No need of output files for specified functional groups... 520 return 521 522 OutfileBasename = OptionsInfo["OutfileBasename"] 523 OutfileExt = OptionsInfo["OutfileExt"] 524 SpecifiedFunctionalGroupsOutfiles = [] 525 526 GroupsCount = len(OptionsInfo["SpecifiedFunctionalGroups"]) 527 for GroupIndex in range(0, GroupsCount): 528 GroupName = OptionsInfo["SpecifiedFunctionalGroups"][GroupIndex] 529 if OptionsInfo["SpecifiedFunctionalGroupsNegateMatch"][GroupIndex]: 530 GroupName = "Not" + GroupName 531 GroupName = re.sub(r"\.", "", GroupName) 532 533 GroupOutfile = "%s%s.%s" % (OutfileBasename, GroupName, OutfileExt) 534 SpecifiedFunctionalGroupsOutfiles.append(GroupOutfile) 535 536 OptionsInfo["SpecifiedFunctionalGroupsOutfiles"] = SpecifiedFunctionalGroupsOutfiles 537 538 539 def RetrieveFunctionalGroupsInfo(): 540 """Retrieve functional groups information.""" 541 542 MiscUtil.PrintInfo( 543 "\nRetrieving data from default RDKit functional groups hierarchy file Functional_Group_Hierarchy.txt..." 544 ) 545 546 FunctionalGroupNamesFile = OptionsInfo["GroupNamesFile"] 547 FunctionalGroupsNodes = FunctionalGroups.BuildFuncGroupHierarchy(FunctionalGroupNamesFile) 548 549 FunctionalGroupsMap["Names"] = [] 550 FunctionalGroupsMap["SMARTSPattern"] = {} 551 552 RetrieveDataFromFunctionalGroupsHierarchy(FunctionalGroupsNodes) 553 554 if not len(FunctionalGroupsMap["Names"]): 555 MiscUtil.PrintError("Failed to retrieve any functional group names and SMARTS patterns...") 556 557 MiscUtil.PrintInfo( 558 "Total number of functional groups present functional group hierarchy: %d" % (len(FunctionalGroupsMap["Names"])) 559 ) 560 561 562 def RetrieveDataFromFunctionalGroupsHierarchy(FGNodes): 563 """Retrieve functional groups data by recursively visiting functional group nodes.""" 564 565 for FGNode in FGNodes: 566 Name = FGNode.label 567 SMARTSPattern = FGNode.smarts 568 569 if Name in FunctionalGroupsMap["SMARTSPattern"]: 570 MiscUtil.PrintWarning("Ignoring duplicate functional group name: %s..." % Name) 571 else: 572 FunctionalGroupsMap["Names"].append(Name) 573 FunctionalGroupsMap["SMARTSPattern"][Name] = SMARTSPattern 574 575 RetrieveDataFromFunctionalGroupsHierarchy(FGNode.children) 576 577 578 def ListFunctionalGroupsInfo(): 579 """List functional groups information.""" 580 581 MiscUtil.PrintInfo("\nListing available functional groups names and SMARTS patterns...") 582 MiscUtil.PrintInfo("\nFunctionalGroupName\tSMARTSPattern") 583 584 for Name in sorted(FunctionalGroupsMap["Names"]): 585 SMARTSPattern = FunctionalGroupsMap["SMARTSPattern"][Name] 586 MiscUtil.PrintInfo("%s\t%s" % (Name, SMARTSPattern)) 587 588 MiscUtil.PrintInfo("") 589 590 591 def ProcessOptions(): 592 """Process and validate command line arguments and options.""" 593 594 MiscUtil.PrintInfo("Processing options...") 595 596 # Validate options... 597 ValidateOptions() 598 599 OptionsInfo["CombineMatches"] = Options["--combineMatches"] 600 601 OptionsInfo["CombineMatchResults"] = True 602 if re.match("^No$", Options["--combineMatches"], re.I): 603 OptionsInfo["CombineMatchResults"] = False 604 if Options["--outfile"]: 605 FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"]) 606 OptionsInfo["OutfileBasename"] = FileName 607 OptionsInfo["OutfileExt"] = FileExt 608 609 OptionsInfo["CombineOperator"] = Options["--combineOperator"] 610 OptionsInfo["AndCombineOperatorMode"] = True 611 if re.match("^or$", Options["--combineOperator"], re.I): 612 OptionsInfo["AndCombineOperatorMode"] = False 613 614 OptionsInfo["GroupNamesFile"] = None 615 if not re.match("^auto$", Options["--groupNamesFile"], re.I): 616 OptionsInfo["GroupNamesFile"] = Options["--groupNamesFile"] 617 618 OptionsInfo["FunctionalGroups"] = Options["--functionalGroups"] 619 620 OptionsInfo["Infile"] = Options["--infile"] 621 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters( 622 "--infileParams", Options["--infileParams"], Options["--infile"] 623 ) 624 625 OptionsInfo["Outfile"] = Options["--outfile"] 626 OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters( 627 "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"] 628 ) 629 630 OptionsInfo["Overwrite"] = Options["--overwrite"] 631 632 OptionsInfo["CountMode"] = False 633 if re.match("^count$", Options["--mode"], re.I): 634 OptionsInfo["CountMode"] = True 635 636 OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False 637 OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"]) 638 639 OptionsInfo["UseChirality"] = False 640 if re.match("^yes$", Options["--useChirality"], re.I): 641 OptionsInfo["UseChirality"] = True 642 643 644 def RetrieveOptions(): 645 """Retrieve command line arguments and options.""" 646 647 # Get options... 648 global Options 649 Options = docopt(_docoptUsage_) 650 651 # Set current working directory to the specified directory... 652 WorkingDir = Options["--workingdir"] 653 if WorkingDir: 654 os.chdir(WorkingDir) 655 656 # Handle examples option... 657 if "--examples" in Options and Options["--examples"]: 658 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_)) 659 sys.exit(0) 660 661 # Handle listing of functional group information... 662 if Options and Options["--list"]: 663 ProcessListFunctionalGroupsOption() 664 sys.exit(0) 665 666 667 def ProcessListFunctionalGroupsOption(): 668 """Process list functional groups information.""" 669 670 # Validate and process dataFile option for listing functional groups information... 671 OptionsInfo["GroupNamesFile"] = None 672 if not re.match("^auto$", Options["--groupNamesFile"], re.I): 673 MiscUtil.ValidateOptionFilePath("-g, --groupNamesFile", Options["--groupNamesFile"]) 674 OptionsInfo["GroupNamesFile"] = Options["--groupNamesFile"] 675 676 RetrieveFunctionalGroupsInfo() 677 ListFunctionalGroupsInfo() 678 679 680 def ValidateOptions(): 681 """Validate option values.""" 682 683 MiscUtil.ValidateOptionTextValue("-c, --combineMatches", Options["--combineMatches"], "yes no") 684 MiscUtil.ValidateOptionTextValue("--combineOperator", Options["--combineOperator"], "and or") 685 686 if not re.match("^auto$", Options["--groupNamesFile"], re.I): 687 MiscUtil.ValidateOptionFilePath("-g, groupNamesFile", Options["--groupNamesFile"]) 688 689 if re.match("^none$", Options["--functionalGroups"], re.I): 690 MiscUtil.PrintError('The name(s) of functional groups must be specified using "-f, --functionalGroups" option') 691 692 MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"]) 693 MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd smi txt csv tsv") 694 if Options["--outfile"]: 695 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi") 696 MiscUtil.ValidateOptionsOutputFileOverwrite( 697 "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"] 698 ) 699 MiscUtil.ValidateOptionsDistinctFileNames( 700 "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"] 701 ) 702 703 MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "retrieve count") 704 if re.match("^retrieve$", Options["--mode"], re.I): 705 if not Options["--outfile"]: 706 MiscUtil.PrintError( 707 'The outfile must be specified using "-o, --outfile" during "retrieve" value of "-m, --mode" option' 708 ) 709 710 MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no") 711 712 MiscUtil.ValidateOptionTextValue("--useChirality", Options["--useChirality"], "yes no") 713 714 715 # Setup a usage string for docopt... 716 _docoptUsage_ = """ 717 RDKitSearchFunctionalGroups.py - Search for functional groups using SMARTS patterns 718 719 Usage: 720 RDKitSearchFunctionalGroups.py [--combineMatches <yes or no>] [--combineOperator <and or or>] 721 [--groupNamesFile <FileName or auto>] [--infileParams <Name,Value,...>] 722 [--mode <retrieve or count>] [--mp <yes or no>] [--mpParams <Name,Value,...>] 723 [--negate <yes or no>] [--outfileParams <Name,Value,...>] [--overwrite] 724 [--useChirality <yes or no>] [-w <dir>] [-o <outfile>] -i <infile> -f <Name1,Name2,Name3... or All> 725 RDKitSearchFunctionalGroups.py [--groupNamesFile <FileName or auto>] -l | --list 726 RDKitSearchFunctionalGroups.py -h | --help | -e | --examples 727 728 Description: 729 Perform a substructure search in an input file using SMARTS patterns for functional 730 groups and write out the matched molecules to an output file or simply count the 731 number of matches. 732 733 The SMARTS patterns for specified functional group(s) are retrieved from file, 734 Functional_Group_Hierarchy.txt, available in RDKit data directory. 735 736 The names of valid functional groups and hierarchies are dynamically retrieved from the 737 functional groups hierarchy file and are shown below: 738 739 AcidChloride, AcidChloride.Aromatic, AcidChloride.Aliphatic 740 Alcohol, Alcohol.Aromatic, Alcohol.Aliphatic 741 Aldehyde, Aldehyde.Aromatic, Aldehyde.Aliphatic 742 Amine, Amine.Primary, Amine.Primary.Aromatic, Amine.Primary.Aliphatic, 743 Amine.Secondary, Amine.Secondary.Aromatic, Amine.Secondary.Aliphatic 744 Amine.Tertiary, Amine.Tertiary.Aromatic, Amine.Tertiary.Aliphatic 745 Amine.Aromatic, Amine.Aliphatic, Amine.Cyclic 746 Azide, Azide.Aromatic, Azide.Aliphatic 747 BoronicAcid, BoronicAcid.Aromatic, BoronicAcid.Aliphatic 748 CarboxylicAcid, CarboxylicAcid.Aromatic, CarboxylicAcid.Aliphatic, 749 CarboxylicAcid.AlphaAmino 750 Halogen, Halogen.Aromatic, Halogen.Aliphatic 751 Halogen.NotFluorine, Halogen.NotFluorine.Aliphatic, 752 Halogen.NotFluorine.Aromatic 753 Halogen.Bromine, Halogen.Bromine.Aliphatic, Halogen.Bromine.Aromatic, 754 Halogen.Bromine.BromoKetone 755 Isocyanate, Isocyanate.Aromatic, Isocyanate.Aliphatic 756 Nitro, Nitro.Aromatic, Nitro.Aliphatic, 757 SulfonylChloride, SulfonylChloride.Aromatic, SulfonylChloride.Aliphatic 758 TerminalAlkyne 759 760 The supported input file formats are: SD (.sdf, .sd), SMILES (.smi, .csv, .tsv, .txt) 761 762 The supported output file formats are: SD (.sdf, .sd), SMILES (.smi) 763 764 Options: 765 -c, --combineMatches <yes or no> [default: yes] 766 Combine search results for matching SMARTS patterns of specified functional groups 767 against a molecule. Possible values: yes or no. 768 769 The matched molecules are written to a single output file for "yes" value. Otherwise, 770 multiple output files are generated, one for each functional group. The names of 771 these files correspond to a combination of the basename of the specified output file 772 and the name of the functional group. 773 774 No output files are generated during "count" value of "-m, --mode" option. 775 --combineOperator <and or or> [default: and] 776 Logical operator to use for combining match results corresponding to specified 777 functional group names before writing out a single file. This option is ignored 778 during "No" value of "-c, --combineMatches" option. 779 -e, --examples 780 Print examples. 781 -g, --groupNamesFile <FileName or auto> [default: auto] 782 Specify a file name containing data for functional groups hierarchy or use functional 783 group hierarchy file, Functional_Group_Hierarchy.txt, available in RDKit data directory. 784 785 RDKit data format: Name<tab>Smarts<tab>Label<tab>RemovalReaction (optional) 786 787 The format of data in local functional group hierarchy must match format of the 788 data in functional group file available in RDKit data directory. 789 -f, --functionalGroups <Name1,Name2,Name3... or All> [default: none] 790 Functional group names for performing substructure SMARTS search. Possible values: 791 Comma delimited list of valid functional group names or All. The current set of valid 792 functional group names are listed in the description section. 793 794 The match results for multiple functional group names are combined using 'and' 795 operator before writing them out to single file. No merging of match results takes 796 place during generation of individual result files corresponding to fictional group 797 names. 798 799 The functional group name may be started with an exclamation mark to negate 800 the match result for that fictional group. 801 -h, --help 802 Print this help message. 803 -i, --infile <infile> 804 Input file name. 805 --infileParams <Name,Value,...> [default: auto] 806 A comma delimited list of parameter name and value pairs for reading 807 molecules from files. The supported parameter names for different file 808 formats, along with their default values, are shown below: 809 810 SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes 811 SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space, 812 smilesTitleLine,auto,sanitize,yes 813 814 Possible values for smilesDelimiter: space, comma or tab. 815 -l, --list 816 List functional groups information without performing any search. 817 -m, --mode <retrieve or count> [default: retrieve] 818 Specify whether to retrieve and write out matched molecules to an output 819 file or simply count the number of matches. 820 --mp <yes or no> [default: no] 821 Use multiprocessing. 822 823 By default, input data is retrieved in a lazy manner via mp.Pool.imap() 824 function employing lazy RDKit data iterable. This allows processing of 825 arbitrary large data sets without any additional requirements memory. 826 827 All input data may be optionally loaded into memory by mp.Pool.map() 828 before starting worker processes in a process pool by setting the value 829 of 'inputDataMode' to 'InMemory' in '--mpParams' option. 830 831 A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input 832 data mode may adversely impact the performance. The '--mpParams' section 833 provides additional information to tune the value of 'chunkSize'. 834 --mpParams <Name,Value,...> [default: auto] 835 A comma delimited list of parameter name and value pairs to configure 836 multiprocessing. 837 838 The supported parameter names along with their default and possible 839 values are shown below: 840 841 chunkSize, auto 842 inputDataMode, Lazy [ Possible values: InMemory or Lazy ] 843 numProcesses, auto [ Default: mp.cpu_count() ] 844 845 These parameters are used by the following functions to configure and 846 control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and 847 mp.Pool.imap(). 848 849 The chunkSize determines chunks of input data passed to each worker 850 process in a process pool by mp.Pool.map() and mp.Pool.imap() functions. 851 The default value of chunkSize is dependent on the value of 'inputDataMode'. 852 853 The mp.Pool.map() function, invoked during 'InMemory' input data mode, 854 automatically converts RDKit data iterable into a list, loads all data into 855 memory, and calculates the default chunkSize using the following method 856 as shown in its code: 857 858 chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4) 859 if extra: chunkSize += 1 860 861 For example, the default chunkSize will be 7 for a pool of 4 worker processes 862 and 100 data items. 863 864 The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs 865 'lazy' RDKit data iterable to retrieve data as needed, without loading all the 866 data into memory. Consequently, the size of input data is not known a priori. 867 It's not possible to estimate an optimal value for the chunkSize. The default 868 chunkSize is set to 1. 869 870 The default value for the chunkSize during 'Lazy' data mode may adversely 871 impact the performance due to the overhead associated with exchanging 872 small chunks of data. It is generally a good idea to explicitly set chunkSize to 873 a larger value during 'Lazy' input data mode, based on the size of your input 874 data and number of processes in the process pool. 875 876 The mp.Pool.map() function waits for all worker processes to process all 877 the data and return the results. The mp.Pool.imap() function, however, 878 returns the the results obtained from worker processes as soon as the 879 results become available for specified chunks of data. 880 881 The order of data in the results returned by both mp.Pool.map() and 882 mp.Pool.imap() functions always corresponds to the input data. 883 -o, --outfile <outfile> 884 Output file name. 885 --outfileParams <Name,Value,...> [default: auto] 886 A comma delimited list of parameter name and value pairs for writing 887 molecules to files. The supported parameter names for different file 888 formats, along with their default values, are shown below: 889 890 SD: compute2DCoords,auto,kekulize,yes,forceV3000,no 891 SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes, 892 smilesTitleLine,yes,smilesMolName,yes,smilesMolProps,no 893 894 Default value for compute2DCoords: yes for SMILES input file; no for all other 895 file types. 896 --overwrite 897 Overwrite existing files. 898 -u, --useChirality <yes or no> [default: no] 899 Use stereochemistry information for SMARTS search. 900 -w, --workingdir <dir> 901 Location of working directory which defaults to the current directory. 902 903 Examples: 904 To list names of all available functional groups along with their SMARTS 905 patterns, type: 906 907 % RDKitSearchFunctionalGroups.py -l 908 909 To retrieve molecules containing amine functional group and write out a 910 SMILES file, type: 911 912 % RDKitSearchFunctionalGroups.py -f Amine -i Sample.smi -o SampleOut.smi 913 914 To retrieve molecules containing amine functional group, perform search in 915 multiprocessing mode on all available CPUs without loading all data into 916 memory, and write out a SMILES file, type: 917 918 % RDKitSearchFunctionalGroups.py --mp yes -f Amine -i Sample.smi 919 -o SampleOut.smi 920 921 To retrieve molecules containing amine functional group, perform search in 922 multiprocessing mode on all available CPUs by loading all data into memory, 923 and write out a SMILES file, type: 924 925 % RDKitSearchFunctionalGroups.py --mp yes --mpParams "inputDataMode, 926 InMemory" -f Amine -i Sample.smi -o SampleOut.smi 927 928 To retrieve molecules containing amine functional group, perform search in 929 multiprocessing mode on specific number of CPUs and chunksize without loading 930 all data into memory, and write out a SMILES file, type: 931 932 % RDKitSearchFunctionalGroups.py --mp yes --mpParams "inputDataMode, 933 lazy,numProcesses,4,chunkSize,8" -f Amine -i Sample.smi -o 934 SampleOut.smi 935 936 To retrieve molecules containing amine functional group but not halogens and carboxylic 937 acid functional groups and write out a SMILES file, type: 938 939 % RDKitSearchFunctionalGroups.py -f 'Amine,!Halogen,!CarboxylicAcid' 940 -i Sample.smi -o SampleOut.smi 941 942 To retrieve molecules containing amine, halogens or carboxylic acid functional groups 943 and write out a SMILES file, type: 944 945 % RDKitSearchFunctionalGroups.py -f 'Amine,Halogen,CarboxylicAcid' 946 --combineOperator or -i Sample.smi -o SampleOut.smi 947 948 To retrieve molecules containing amine and carboxylic acid functional groups defined in 949 a local functional groups hierarchy file and write out individual SD files for each 950 funcitonal group, type: 951 952 % RDKitSearchFunctionalGroups.py -f 'Amine,CarboxylicAcid' -i Sample.sdf 953 -g Custom_Functional_Group_Hierarchy.txt --combineMatches No -o SampleOut.sdf 954 955 To count number of all functional groups in molecules without writing out an output 956 files, type: 957 958 % RDKitSearchFunctionalGroups.py -m count -f All --combineMatches no -i Sample.smi 959 960 To retrieve molecule not containing aromatic alcohol and aromatic halogen functional 961 group along with the use of chirality during substructure search and write out individual 962 SMILES files for each functional group, type: 963 964 % RDKitSearchFunctionalGroups.py --combineMatches no -u yes 965 -f '!Alcohol.Aromatic,!Halogen.Aromatic' -i Sample.smi -o SampleOut.smi 966 967 To retrieve molecule containing amine functional group from a CSV SMILES file, 968 SMILES strings in column 1, name in column 2, and write out a SD file, type: 969 970 % RDKitSearchFunctionalGroups.py -f Amine --infileParams 971 "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1, 972 smilesNameColumn,2" --outfileParams "compute2DCoords,yes" 973 -i SampleSMILES.csv -o SampleOut.sdf 974 975 Author: 976 Manish Sud(msud@san.rr.com) 977 978 See also: 979 RDKitConvertFileFormat.py, RDKitFilterPAINS.py, RDKitSearchSMARTS.py 980 981 Copyright: 982 Copyright (C) 2026 Manish Sud. All rights reserved. 983 984 The functionality available in this script is implemented using RDKit, an 985 open source toolkit for cheminformatics developed by Greg Landrum. 986 987 This file is part of MayaChemTools. 988 989 MayaChemTools is free software; you can redistribute it and/or modify it under 990 the terms of the GNU Lesser General Public License as published by the Free 991 Software Foundation; either version 3 of the License, or (at your option) any 992 later version. 993 994 """ 995 996 if __name__ == "__main__": 997 main()