MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: RDKitPerformRGroupDecomposition.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 
  36 # RDKit imports...
  37 try:
  38     from rdkit import rdBase
  39     from rdkit import Chem
  40     from rdkit.Chem import AllChem
  41     from rdkit.Chem import rdRGroupDecomposition as rgd
  42     from rdkit.Chem import rdFMCS
  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 
  64 def main():
  65     """Start execution of the script."""
  66 
  67     MiscUtil.PrintInfo(
  68         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
  69         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
  70     )
  71 
  72     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  73 
  74     # Retrieve command line arguments and options...
  75     RetrieveOptions()
  76 
  77     # Process and validate command line arguments and options...
  78     ProcessOptions()
  79 
  80     # Perform actions required by the script...
  81     PerformRGroupDecomposition()
  82 
  83     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  84     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  85 
  86 
  87 def PerformRGroupDecomposition():
  88     """Perform R group decomposition."""
  89 
  90     # Retrieve molecules...
  91     Mols = RetrieveMolecules()
  92 
  93     # Identify R groups and write them out...
  94     RGroups, UnmatchedMolIndices = RetrieveRGroups(Mols)
  95     WriteRGroups(Mols, RGroups, UnmatchedMolIndices)
  96 
  97 
  98 def RetrieveRGroups(Mols):
  99     """Retrieve R groups."""
 100 
 101     CoreMols = SetupCoreScaffolds(Mols)
 102     DecompositionParams = SetupRGroupDecompositionParams()
 103     RGroupDecompositionObject = rgd.RGroupDecomposition(CoreMols, DecompositionParams)
 104 
 105     MiscUtil.PrintInfo("\nPerforming R group decomposition...")
 106 
 107     UnmatchedMolIndices = []
 108     for MolIndex, Mol in enumerate(Mols):
 109         Status = RGroupDecompositionObject.Add(Mol)
 110         if Status < 0:
 111             UnmatchedMolIndices.append(MolIndex)
 112 
 113     if not RGroupDecompositionObject.Process():
 114         MiscUtil.PrintWarning("R group decomposition failed to match any molecule to core scaffold(s)...")
 115 
 116     RGroups = RGroupDecompositionObject.GetRGroupsAsColumns(asSmiles=True)
 117 
 118     return (RGroups, UnmatchedMolIndices)
 119 
 120 
 121 def SetupCoreScaffolds(Mols):
 122     """Setup core scaffold molecules(s)."""
 123 
 124     if re.match("^(BySMARTS|BySMILES)$", OptionsInfo["CoreScaffold"], re.I):
 125         return SetupCoreScaffoldsBySMARTSOrSMILES()
 126     elif re.match("^ByMCS$", OptionsInfo["CoreScaffold"], re.I):
 127         return SetupCoreScaffoldsByMCS(Mols)
 128     else:
 129         MiscUtil.PrintError(
 130             'The  value, %s, specified for  "-c, --coreScaffold" option is not supported.'
 131             % (OptionsInfo["CoreScaffold"])
 132         )
 133 
 134 
 135 def SetupCoreScaffoldsBySMARTSOrSMILES():
 136     """Setup core scaffold molecules(s) using specified SMARTS or SMILES."""
 137 
 138     BySMARTS = True if re.match("^BySMARTS$", OptionsInfo["CoreScaffold"], re.I) else False
 139     CoreScaffoldList = OptionsInfo["SMARTSOrSMILESCoreScaffoldList"]
 140 
 141     if BySMARTS:
 142         MiscUtil.PrintInfo(
 143             "\nSetting up core scaffold(s) using SMARTS...\nSMARTS core scaffold(s): %s" % " ".join(CoreScaffoldList)
 144         )
 145     else:
 146         MiscUtil.PrintInfo(
 147             "\nSetting up core scaffold(s) using SMILES...\nSMILES core scaffold(s): %s" % " ".join(CoreScaffoldList)
 148         )
 149 
 150     CoreMols = []
 151     for Core in CoreScaffoldList:
 152         if BySMARTS:
 153             CoreMol = Chem.MolFromSmarts(Core)
 154         else:
 155             CoreMol = Chem.MolFromSmiles(Core)
 156         if CoreMol is None:
 157             MiscUtil.PrintError("Failed to generate mol for core scaffold: %s" % (Core))
 158         CoreMols.append(CoreMol)
 159 
 160     return CoreMols
 161 
 162 
 163 def SetupCoreScaffoldsByMCS(Mols):
 164     """Setup core scaffold molecule using MCS."""
 165 
 166     MiscUtil.PrintInfo("\nSetting up core scaffold using MCS...")
 167 
 168     MCSParams = OptionsInfo["MCSParams"]
 169 
 170     CoreMols = []
 171 
 172     MCSResultObject = rdFMCS.FindMCS(
 173         Mols,
 174         maximizeBonds=MCSParams["MaximizeBonds"],
 175         threshold=MCSParams["Threshold"],
 176         timeout=MCSParams["TimeOut"],
 177         verbose=MCSParams["Verbose"],
 178         matchValences=MCSParams["MatchValences"],
 179         ringMatchesRingOnly=MCSParams["RingMatchesRingOnly"],
 180         completeRingsOnly=MCSParams["CompleteRingsOnly"],
 181         matchChiralTag=MCSParams["MatchChiralTag"],
 182         atomCompare=MCSParams["AtomCompare"],
 183         bondCompare=MCSParams["BondCompare"],
 184         seedSmarts=MCSParams["SeedSMARTS"],
 185     )
 186 
 187     if MCSResultObject.canceled:
 188         MiscUtil.PrintError(
 189             'MCS failed to identify a core scaffold. Specify a different set of parameters using "-m, --mcsParams" option and try again.'
 190         )
 191 
 192     CoreNumAtoms = MCSResultObject.numAtoms
 193     CoreNumBonds = MCSResultObject.numBonds
 194     SMARTSCore = MCSResultObject.smartsString
 195 
 196     if not len(SMARTSCore):
 197         MiscUtil.PrintError(
 198             'MCS failed to identify a core scaffold. Specify a different set of parameters using "-m, --mcsParams" option and try again.'
 199         )
 200 
 201     MiscUtil.PrintInfo(
 202         "SMARTS core scaffold: %s\nNumber of atoms in core scaffold: %s\nNumber of bonds in core scaffold: %s"
 203         % (SMARTSCore, CoreNumAtoms, CoreNumBonds)
 204     )
 205 
 206     if CoreNumAtoms < MCSParams["MinNumAtoms"]:
 207         MiscUtil.PrintError(
 208             'Number of atoms, %d, in core scaffold identified by MCS is less than, %d, as specified by "minNumAtoms" parameter in  "-m, --mcsParams" option.'
 209             % (CoreNumAtoms, MCSParams["MinNumAtoms"])
 210         )
 211 
 212     if CoreNumBonds < MCSParams["MinNumBonds"]:
 213         MiscUtil.PrintError(
 214             'Number of bonds, %d, in core scaffold identified by MCS is less than, %d, as specified by "minNumBonds" parameter in  "-m, --mcsParams" option.'
 215             % (CoreNumBonds, MCSParams["MinNumBonds"])
 216         )
 217 
 218     CoreMol = Chem.MolFromSmarts(SMARTSCore)
 219     CoreMols.append(CoreMol)
 220 
 221     return CoreMols
 222 
 223 
 224 def SetupRGroupDecompositionParams():
 225     """Setup R group decomposition parameters."""
 226 
 227     DecompositionParams = rgd.RGroupDecompositionParameters()
 228 
 229     DecompositionParams.alignment = OptionsInfo["DecompositionParams"]["RGroupCoreAlignment"]
 230     DecompositionParams.chunkSize = OptionsInfo["DecompositionParams"]["chunkSize"]
 231     DecompositionParams.matchingStrategy = OptionsInfo["DecompositionParams"]["RGroupMatching"]
 232     DecompositionParams.onlyMatchAtRGroups = OptionsInfo["DecompositionParams"]["matchOnlyAtRGroups"]
 233     DecompositionParams.removeAllHydrogenRGroups = OptionsInfo["DecompositionParams"]["removeHydrogenOnlyGroups"]
 234     DecompositionParams.removeHydrogensPostMatch = OptionsInfo["DecompositionParams"]["removeHydrogensPostMatch"]
 235 
 236     return DecompositionParams
 237 
 238 
 239 def WriteRGroups(Mols, RGroups, UnmatchedMolIndices):
 240     """Write out R groups."""
 241 
 242     Outfile = OptionsInfo["Outfile"]
 243     UnmatchedOutfile = OptionsInfo["UnmatchedOutfile"]
 244     RemoveUnmatchedMode = OptionsInfo["RemoveUnmatchedMode"]
 245 
 246     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 247 
 248     TextOutFileMode = OptionsInfo["TextOutFileMode"]
 249     TextOutFileDelim = OptionsInfo["TextOutFileDelim"]
 250     Quote = OptionsInfo["TextOutQuote"]
 251 
 252     SMILESIsomeric = OptionsInfo["OutfileParams"]["SMILESIsomeric"]
 253     SMILESKekulize = OptionsInfo["OutfileParams"]["SMILESKekulize"]
 254 
 255     # Setup writers...
 256     Writer = None
 257     UnmatchedWriter = None
 258     if TextOutFileMode:
 259         Writer = open(Outfile, "w")
 260         if RemoveUnmatchedMode:
 261             UnmatchedWriter = open(UnmatchedOutfile, "w")
 262     else:
 263         Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
 264         if RemoveUnmatchedMode:
 265             UnmatchedWriter = RDKitUtil.MoleculesWriter(UnmatchedOutfile, **OptionsInfo["OutfileParams"])
 266 
 267     if Writer is None:
 268         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 269     if RemoveUnmatchedMode:
 270         if UnmatchedWriter is None:
 271             MiscUtil.PrintError("Failed to setup a writer for output fie %s " % UnmatchedOutfile)
 272 
 273     if RemoveUnmatchedMode:
 274         MiscUtil.PrintInfo("\nGenerating files: %s %s..." % (Outfile, UnmatchedOutfile))
 275     else:
 276         MiscUtil.PrintInfo("\nGenerating file %s..." % Outfile)
 277 
 278     # Set up data keys and labels for  core and R groups...
 279     RGroupsDataKeys = []
 280     RGroupsDataLabels = []
 281     CoreDataLabelPresent = False
 282     for DataLabel in sorted(RGroups):
 283         if re.match("^Core", DataLabel, re.I):
 284             CoreDataLabelPresent = True
 285             RGroupsDataKeys.append(DataLabel)
 286             RGroupsDataLabels.append("SMILES%s" % DataLabel)
 287         elif re.match("^R", DataLabel, re.I):
 288             RGroupsDataKeys.append(DataLabel)
 289             RGroupsDataLabels.append("SMILES%s" % DataLabel)
 290         else:
 291             MiscUtil.PrintWarning(
 292                 "Ignoring unknown R group data label, %s, found during R group decomposition..." % DataLabel
 293             )
 294 
 295     if CoreDataLabelPresent:
 296         RGroupsCategoriesCount = len(RGroupsDataLabels) - 1
 297     else:
 298         RGroupsCategoriesCount = len(RGroupsDataLabels)
 299     RGroupsMolUnmatchedCount = len(UnmatchedMolIndices)
 300     RGroupsMolMatchedCount = len(Mols) - RGroupsMolUnmatchedCount
 301 
 302     # Wite out headers for a text file...
 303     if TextOutFileMode:
 304         LineWords = ["SMILES", "Name"]
 305 
 306         if RemoveUnmatchedMode:
 307             Line = MiscUtil.JoinWords(LineWords, TextOutFileDelim, Quote)
 308             UnmatchedWriter.write("%s\n" % Line)
 309 
 310         LineWords.extend(RGroupsDataLabels)
 311         Line = MiscUtil.JoinWords(LineWords, TextOutFileDelim, Quote)
 312         Writer.write("%s\n" % Line)
 313 
 314     MolCount = 0
 315     RGroupsResultIndex = -1
 316 
 317     for MolIndex, Mol in enumerate(Mols):
 318         MolCount += 1
 319 
 320         UnmatchedMol = False
 321         if MolIndex in UnmatchedMolIndices:
 322             UnmatchedMol = True
 323 
 324         if UnmatchedMol:
 325             RGroupsDataSMILES = [""] * len(RGroupsDataKeys)
 326         else:
 327             RGroupsResultIndex += 1
 328             RGroupsDataSMILES = [RGroups[RGroupsDataKey][RGroupsResultIndex] for RGroupsDataKey in RGroupsDataKeys]
 329 
 330         if TextOutFileMode:
 331             # Write out text file including SMILES file...
 332             MolSMILES = Chem.MolToSmiles(Mol, isomericSmiles=SMILESIsomeric, kekuleSmiles=SMILESKekulize)
 333             MolName = RDKitUtil.GetMolName(Mol, MolCount)
 334             LineWords = [MolSMILES, MolName]
 335 
 336             if UnmatchedMol and RemoveUnmatchedMode:
 337                 Line = MiscUtil.JoinWords(LineWords, TextOutFileDelim, Quote)
 338                 UnmatchedWriter.write("%s\n" % Line)
 339             else:
 340                 LineWords.extend(RGroupsDataSMILES)
 341                 Line = MiscUtil.JoinWords(LineWords, TextOutFileDelim, Quote)
 342                 Writer.write("%s\n" % Line)
 343         else:
 344             # Write out SD file...
 345             if Compute2DCoords:
 346                 AllChem.Compute2DCoords(Mol)
 347 
 348             if UnmatchedMol and RemoveUnmatchedMode:
 349                 UnmatchedWriter.write(Mol)
 350             else:
 351                 for Name, Value in zip(RGroupsDataLabels, RGroupsDataSMILES):
 352                     Mol.SetProp(Name, Value)
 353                 Writer.write(Mol)
 354 
 355     if Writer is not None:
 356         Writer.close()
 357     if UnmatchedWriter is not None:
 358         UnmatchedWriter.close()
 359 
 360     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
 361     MiscUtil.PrintInfo("Number of  R group categories: %d" % RGroupsCategoriesCount)
 362     MiscUtil.PrintInfo("Number of  matched molecules containing core scaffold(s): %d" % RGroupsMolMatchedCount)
 363     MiscUtil.PrintInfo("Number of  unmatched molecules containing no core scaffold(s): %d" % RGroupsMolUnmatchedCount)
 364 
 365 
 366 def RetrieveMolecules():
 367     """Retrieve molecules."""
 368 
 369     Infile = OptionsInfo["Infile"]
 370 
 371     # Read molecules...
 372     MiscUtil.PrintInfo("\nReading file %s..." % Infile)
 373     OptionsInfo["InfileParams"]["AllowEmptyMols"] = False
 374     ValidMols, MolCount, ValidMolCount = RDKitUtil.ReadAndValidateMolecules(Infile, **OptionsInfo["InfileParams"])
 375 
 376     MiscUtil.PrintInfo("Total number of molecules: %d" % MolCount)
 377     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
 378     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
 379 
 380     return ValidMols
 381 
 382 
 383 def ProcessDecompositionParameters():
 384     """Set up and process decomposition parameters."""
 385 
 386     SetupDecompositionParameters()
 387     ProcessSpecifiedDecompositionParameters()
 388 
 389 
 390 def SetupDecompositionParameters():
 391     """Set up default decomposition parameters."""
 392 
 393     OptionsInfo["DecompositionParams"] = {
 394         "RGroupCoreAlignment": rgd.RGroupCoreAlignment.MCS,
 395         "RGroupMatching": rgd.RGroupMatching.GreedyChunks,
 396         "chunkSize": 5,
 397         "matchOnlyAtRGroups": False,
 398         "removeHydrogenOnlyGroups": True,
 399         "removeHydrogensPostMatch": False,
 400     }
 401 
 402 
 403 def ProcessSpecifiedDecompositionParameters():
 404     """Process specified decomposition parameters."""
 405 
 406     if re.match("^auto$", OptionsInfo["SpecifiedDecompositionParams"], re.I):
 407         # Nothing to process...
 408         return
 409 
 410     # Parse specified parameters...
 411     DecompositionParams = re.sub(" ", "", OptionsInfo["SpecifiedDecompositionParams"])
 412     if not DecompositionParams:
 413         MiscUtil.PrintError(
 414             'No valid parameter name and value pairs specified using "-d, --decompositionParams" option.'
 415         )
 416 
 417     DecompositionParamsWords = DecompositionParams.split(",")
 418     if len(DecompositionParamsWords) % 2:
 419         MiscUtil.PrintError(
 420             'The number of comma delimited paramater names and values, %d, specified using "-d, --decompositionParams" option must be an even number.'
 421             % (len(DecompositionParamsWords))
 422         )
 423 
 424     # Setup  canonical parameter names...
 425     ValidParamNames = []
 426     CanonicalParamNamesMap = {}
 427     for ParamName in sorted(OptionsInfo["DecompositionParams"]):
 428         ValidParamNames.append(ParamName)
 429         CanonicalParamNamesMap[ParamName.lower()] = ParamName
 430 
 431     # Validate and set paramater names and value...
 432     for Index in range(0, len(DecompositionParamsWords), 2):
 433         Name = DecompositionParamsWords[Index]
 434         Value = DecompositionParamsWords[Index + 1]
 435 
 436         CanonicalName = Name.lower()
 437         if CanonicalName not in CanonicalParamNamesMap:
 438             MiscUtil.PrintError(
 439                 'The parameter name, %s, specified using "-d, --decompositionParams" option is not a valid name. Supported parameter names: %s'
 440                 % (Name, " ".join(ValidParamNames))
 441             )
 442 
 443         ParamName = CanonicalParamNamesMap[CanonicalName]
 444         if re.match("^RGroupCoreAlignment$", ParamName, re.I):
 445             if re.match("^MCS$", Value, re.I):
 446                 ParamValue = rgd.RGroupCoreAlignment.MCS
 447             elif re.match("^None$", Value, re.I):
 448                 ParamValue = rgd.RGroupCoreAlignment.names["None"]
 449             else:
 450                 MiscUtil.PrintError(
 451                     'The parameter value, %s, specified using "-d, --decompositionParams" option  for parameter, %s, is not a valid value. Supported values: MCS None'
 452                     % (Value, Name)
 453                 )
 454         elif re.match("^RGroupMatching$", ParamName, re.I):
 455             if re.match("^Greedy$", Value, re.I):
 456                 ParamValue = rgd.RGroupMatching.Greedy
 457             elif re.match("^GreedyChunks$", Value, re.I):
 458                 ParamValue = rgd.RGroupMatching.GreedyChunks
 459             elif re.match("^Exhaustive$", Value, re.I):
 460                 ParamValue = rgd.RGroupMatching.Exhaustive
 461             else:
 462                 MiscUtil.PrintError(
 463                     'The parameter value, %s, specified using "-d, --decompositionParams" option  for parameter, %s, is not a valid value. Supported values: Greedy GreedyChunks Exhaustive'
 464                     % (Value, Name)
 465                 )
 466         elif re.match("^chunkSize$", ParamName, re.I):
 467             Value = int(Value)
 468             if Value <= 0:
 469                 MiscUtil.PrintError(
 470                     'The parameter value, %s, specified using "-d, --decompositionParams" option  for parameter, %s, is not a valid value. Supported values: > 0'
 471                     % (Value, Name)
 472                 )
 473             ParamValue = Value
 474         else:
 475             if not re.match("^(Yes|No|True|False)$", Value, re.I):
 476                 MiscUtil.PrintError(
 477                     'The parameter value, %s, specified using "-d, --decompositionParams" option  for parameter, %s, is not a valid value. Supported values: Yes No True False'
 478                     % (Value, Name)
 479                 )
 480             ParamValue = False
 481             if re.match("^(Yes|True)$", Value, re.I):
 482                 ParamValue = True
 483 
 484         # Set value...
 485         OptionsInfo["DecompositionParams"][ParamName] = ParamValue
 486 
 487 
 488 def ProcessMCSParameters():
 489     """Set up and process MCS parameters."""
 490 
 491     SetupMCSParameters()
 492     ProcessSpecifiedMCSParameters()
 493 
 494 
 495 def SetupMCSParameters():
 496     """Set up default MCS parameters."""
 497 
 498     OptionsInfo["MCSParams"] = {
 499         "MaximizeBonds": True,
 500         "Threshold": 0.9,
 501         "TimeOut": 3600,
 502         "Verbose": False,
 503         "MatchValences": True,
 504         "MatchChiralTag": False,
 505         "RingMatchesRingOnly": True,
 506         "CompleteRingsOnly": True,
 507         "AtomCompare": rdFMCS.AtomCompare.CompareElements,
 508         "BondCompare": rdFMCS.BondCompare.CompareOrder,
 509         "SeedSMARTS": "",
 510         "MinNumAtoms": 1,
 511         "MinNumBonds": 0,
 512     }
 513 
 514 
 515 def ProcessSpecifiedMCSParameters():
 516     """Process specified MCS parameters."""
 517 
 518     if re.match("^auto$", OptionsInfo["SpecifiedMCSParams"], re.I):
 519         # Nothing to process...
 520         return
 521 
 522     # Parse specified parameters...
 523     MCSParams = re.sub(" ", "", OptionsInfo["SpecifiedMCSParams"])
 524     if not MCSParams:
 525         MiscUtil.PrintError('No valid parameter name and value pairs specified using "-m, --mcsParams" option.')
 526 
 527     MCSParamsWords = MCSParams.split(",")
 528     if len(MCSParamsWords) % 2:
 529         MiscUtil.PrintError(
 530             'The number of comma delimited paramater names and values, %d, specified using "-m, --mcsParams" option must be an even number.'
 531             % (len(MCSParamsWords))
 532         )
 533 
 534     # Setup  canonical parameter names...
 535     ValidParamNames = []
 536     CanonicalParamNamesMap = {}
 537     for ParamName in sorted(OptionsInfo["MCSParams"]):
 538         ValidParamNames.append(ParamName)
 539         CanonicalParamNamesMap[ParamName.lower()] = ParamName
 540 
 541     # Validate and set paramater names and value...
 542     for Index in range(0, len(MCSParamsWords), 2):
 543         Name = MCSParamsWords[Index]
 544         Value = MCSParamsWords[Index + 1]
 545 
 546         CanonicalName = Name.lower()
 547         if CanonicalName not in CanonicalParamNamesMap:
 548             MiscUtil.PrintError(
 549                 'The parameter name, %s, specified using "-m, --mcsParams" option is not a valid name. Supported parameter names: %s'
 550                 % (Name, " ".join(ValidParamNames))
 551             )
 552 
 553         ParamName = CanonicalParamNamesMap[CanonicalName]
 554         if re.match("^Threshold$", ParamName, re.I):
 555             Value = float(Value)
 556             if Value <= 0.0 or Value > 1.0:
 557                 MiscUtil.PrintError(
 558                     'The parameter value, %s, specified using "-m, --mcsParams" option  for parameter, %s, is not a valid value. Supported values: > 0 and <= 1.0'
 559                     % (Value, Name)
 560                 )
 561             ParamValue = Value
 562         elif re.match("^Timeout$", ParamName, re.I):
 563             Value = int(Value)
 564             if Value <= 0:
 565                 MiscUtil.PrintError(
 566                     'The parameter value, %s, specified using "-m, --mcsParams" option  for parameter, %s, is not a valid value. Supported values: > 0'
 567                     % (Value, Name)
 568                 )
 569             ParamValue = Value
 570         elif re.match("^MinNumAtoms$", ParamName, re.I):
 571             Value = int(Value)
 572             if Value < 1:
 573                 MiscUtil.PrintError(
 574                     'The parameter value, %s, specified using "-m, --mcsParams" option  for parameter, %s, is not a valid value. Supported values: >= 1'
 575                     % (Value, Name)
 576                 )
 577             ParamValue = Value
 578         elif re.match("^MinNumBonds$", ParamName, re.I):
 579             Value = int(Value)
 580             if Value < 0:
 581                 MiscUtil.PrintError(
 582                     'The parameter value, %s, specified using "-m, --mcsParams" option  for parameter, %s, is not a valid value. Supported values: >=0 '
 583                     % (Value, Name)
 584                 )
 585             ParamValue = Value
 586         elif re.match("^AtomCompare$", ParamName, re.I):
 587             if re.match("^CompareAny$", Value, re.I):
 588                 ParamValue = rdFMCS.AtomCompare.CompareAny
 589             elif re.match("^CompareElements$", Value, re.I):
 590                 ParamValue = Chem.rdFMCS.AtomCompare.CompareElements
 591             elif re.match("^CompareIsotopes$", Value, re.I):
 592                 ParamValue = Chem.rdFMCS.AtomCompare.CompareIsotopes
 593             else:
 594                 MiscUtil.PrintError(
 595                     'The parameter value, %s, specified using "-m, --mcsParams" option  for parameter, %s, is not a valid value. Supported values: CompareAny CompareElements CompareIsotopes'
 596                     % (Value, Name)
 597                 )
 598         elif re.match("^BondCompare$", ParamName, re.I):
 599             if re.match("^CompareAny$", Value, re.I):
 600                 ParamValue = Chem.rdFMCS.BondCompare.CompareAny
 601             elif re.match("^CompareOrder$", Value, re.I):
 602                 ParamValue = rdFMCS.BondCompare.CompareOrder
 603             elif re.match("^CompareOrderExact$", Value, re.I):
 604                 ParamValue = rdFMCS.BondCompare.CompareOrderExact
 605             else:
 606                 MiscUtil.PrintError(
 607                     'The parameter value, %s, specified using "-m, --mcsParams" option  for parameter, %s, is not a valid value. Supported values: CompareAny CompareOrder CompareOrderExact'
 608                     % (Value, Name)
 609                 )
 610         elif re.match("^SeedSMARTS$", ParamName, re.I):
 611             if not len(Value):
 612                 MiscUtil.PrintError(
 613                     'The parameter value specified using "-m, --mcsParams" option  for parameter, %s, is empty. '
 614                     % (Name)
 615                 )
 616             ParamValue = Value
 617         else:
 618             if not re.match("^(Yes|No|True|False)$", Value, re.I):
 619                 MiscUtil.PrintError(
 620                     'The parameter value, %s, specified using "-m, --mcsParams" option  for parameter, %s, is not a valid value. Supported values: Yes No True False'
 621                     % (Value, Name)
 622                 )
 623             ParamValue = False
 624             if re.match("^(Yes|True)$", Value, re.I):
 625                 ParamValue = True
 626 
 627         # Set value...
 628         OptionsInfo["MCSParams"][ParamName] = ParamValue
 629 
 630 
 631 def ProcessOptions():
 632     """Process and validate command line arguments and options."""
 633 
 634     MiscUtil.PrintInfo("Processing options...")
 635 
 636     # Validate options...
 637     ValidateOptions()
 638 
 639     OptionsInfo["CoreScaffold"] = Options["--coreScaffold"]
 640 
 641     OptionsInfo["Infile"] = Options["--infile"]
 642     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 643         "--infileParams", Options["--infileParams"], Options["--infile"]
 644     )
 645 
 646     OptionsInfo["Outfile"] = Options["--outfile"]
 647     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 648         "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"]
 649     )
 650 
 651     TextOutFileMode = False
 652     TextOutFileDelim = ""
 653 
 654     if MiscUtil.CheckFileExt(Options["--outfile"], "csv"):
 655         TextOutFileMode = True
 656         TextOutFileDelim = ","
 657     elif MiscUtil.CheckFileExt(Options["--outfile"], "tsv txt"):
 658         TextOutFileMode = True
 659         TextOutFileDelim = "\t"
 660 
 661     OptionsInfo["TextOutFileMode"] = TextOutFileMode
 662     OptionsInfo["TextOutFileDelim"] = TextOutFileDelim
 663 
 664     TextOutQuote = False
 665     if re.match("^auto$", Options["--quote"], re.I):
 666         if MiscUtil.CheckFileExt(Options["--outfile"], "csv"):
 667             TextOutQuote = True
 668     else:
 669         if re.match("^yes$", Options["--quote"], re.I):
 670             TextOutQuote = True
 671     OptionsInfo["TextOutQuote"] = TextOutQuote
 672 
 673     OptionsInfo["Overwrite"] = Options["--overwrite"]
 674 
 675     RemoveUnmatchedMode = False
 676     UnmatchedOutfile = None
 677     if re.match("^yes$", Options["--removeUnmatched"], re.I):
 678         RemoveUnmatchedMode = True
 679         FileDir, FileName, FileExt = MiscUtil.ParseFileName(OptionsInfo["Outfile"])
 680         UnmatchedOutfile = "%sUnmatched.%s" % (FileName, FileExt)
 681     OptionsInfo["RemoveUnmatchedMode"] = RemoveUnmatchedMode
 682     OptionsInfo["UnmatchedOutfile"] = UnmatchedOutfile
 683 
 684     OptionsInfo["SpecifiedDecompositionParams"] = Options["--decompositionParams"]
 685     ProcessDecompositionParameters()
 686 
 687     OptionsInfo["SpecifiedMCSParams"] = Options["--mcsParams"]
 688     ProcessMCSParameters()
 689 
 690     SMARTSOrSMILESCoreScaffold = ""
 691     SMARTSOrSMILESCoreScaffoldList = []
 692     if not re.match("^none$", Options["--smartsOrSmilesCoreScaffold"], re.I) or len(
 693         Options["--smartsOrSmilesCoreScaffold"]
 694     ):
 695         if re.match("^(BySMARTS|BySMILES)$", Options["--coreScaffold"], re.I):
 696             SMARTSOrSMILESCoreScaffold = re.sub(" ", "", Options["--smartsOrSmilesCoreScaffold"])
 697             if not SMARTSOrSMILESCoreScaffold:
 698                 MiscUtil.PrintError(
 699                     'A non empty value must be specified for "-s, --smartsOrSmilesCoreScaffold" during %s value of "-c, --coreScaffold" option '
 700                     % (Options["--coreScaffold"])
 701                 )
 702             SMARTSOrSMILESCoreScaffoldList = SMARTSOrSMILESCoreScaffold.split(",")
 703     OptionsInfo["SMARTSOrSMILESCoreScaffold"] = SMARTSOrSMILESCoreScaffold
 704     OptionsInfo["SMARTSOrSMILESCoreScaffoldList"] = SMARTSOrSMILESCoreScaffoldList
 705 
 706 
 707 def RetrieveOptions():
 708     """Retrieve command line arguments and options."""
 709 
 710     # Get options...
 711     global Options
 712     Options = docopt(_docoptUsage_)
 713 
 714     # Set current working directory to the specified directory...
 715     WorkingDir = Options["--workingdir"]
 716     if WorkingDir:
 717         os.chdir(WorkingDir)
 718 
 719     # Handle examples option...
 720     if "--examples" in Options and Options["--examples"]:
 721         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 722         sys.exit(0)
 723 
 724 
 725 def ValidateOptions():
 726     """Validate option values."""
 727 
 728     MiscUtil.ValidateOptionTextValue("-c, --coreScaffold", Options["--coreScaffold"], "ByMCS BySMARTS BySMILES")
 729 
 730     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 731     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi csv tsv txt")
 732 
 733     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd csv tsv txt")
 734     MiscUtil.ValidateOptionsOutputFileOverwrite(
 735         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 736     )
 737     MiscUtil.ValidateOptionsDistinctFileNames(
 738         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 739     )
 740 
 741     if re.match("^none$", Options["--smartsOrSmilesCoreScaffold"], re.I) or not len(
 742         Options["--smartsOrSmilesCoreScaffold"]
 743     ):
 744         if re.match("^(BySMARTS|BySMILES)$", Options["--coreScaffold"], re.I):
 745             MiscUtil.PrintError(
 746                 'A non empty value must be specified for "-s, --smartsOrSmilesCoreScaffold" during %s value of "-c, --coreScaffold" option '
 747                 % (Options["--coreScaffold"])
 748             )
 749     else:
 750         if not re.match("^(BySMARTS|BySMILES)$", Options["--coreScaffold"], re.I):
 751             MiscUtil.PrintError(
 752                 '%s value of "-s, --smartsOrSmilesCoreScaffold" is not allowed during %s value of "-c, --coreScaffold" option '
 753                 % (Options["--smartsOrSmilesCoreScaffold"], Options["--coreScaffold"])
 754             )
 755 
 756     MiscUtil.ValidateOptionTextValue("-q, --quote", Options["--quote"], "yes no auto")
 757     MiscUtil.ValidateOptionTextValue("-r, --removeUnmatched", Options["--removeUnmatched"], "yes no")
 758 
 759 
 760 # Setup a usage string for docopt...
 761 _docoptUsage_ = """
 762 RDKitPerformRGroupDecomposition.py - Perform R group decomposition analysis
 763 
 764 Usage:
 765     RDKitPerformRGroupDecomposition.py [--coreScaffold <ByMCS, BySMARTS or BySMILES>]
 766                                        [--decompositionParams <Name,Value,...>]
 767                                        [--infileParams <Name,Value,...>] [--mcsParams <Name,Value,...>]
 768                                        [--outfileParams <Name,Value,...>] [--overwrite] [--quote <yes or no>]
 769                                        [--removeUnmatched <yes or no>] [--smartsOrSmilesCoreScaffold <text>]
 770                                        [-w <dir>] -i <infile> -o <outfile> 
 771     RDKitPerformRGroupDecomposition.py -h | --help | -e | --examples
 772 
 773 Description:
 774     Perform R group decomposition for a set of molecules in a series containing
 775     a common core scaffold. The core scaffold is identified by a SMARTS string,
 776     SMILES string, or using maximum common substructure (MCS) search.
 777     Multiple core scaffolds may be specified using SMARTS or SMILES strings for
 778     set of molecules corresponding to multiple series.
 779 
 780     The core scaffolds along with appropriate R groups are written out as SMILES
 781     strings to a SD or text file. The unmatched molecules without any specified
 782     core scaffold are written to a different output file.
 783 
 784     The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
 785     .txt, .csv, .tsv)
 786 
 787     The supported output file formats are: SD (.sdf, .sd), CSV/TSV (.csv, .tsv, .txt)
 788 
 789 Options:
 790     -c, --coreScaffold <ByMCS, BySMARTS or BySMILES>  [default: ByMCS]
 791         Specify a core scaffold for a set of molecules in a series. The core scaffold
 792         is identified by an explicit SMARTS string, SMILES string, or using maximum
 793         common substructure (MCS) search. Multiple core scaffolds may be specified
 794         using SMARTS or SMILES strings for set of molecules corresponding to multiple
 795         series.
 796     -d, --decompositionParams <Name,Value,...>  [default: auto]
 797         Parameter values to use during R group decomposition for a series of molecules.
 798         In general, it is a comma delimited list of parameter name and value pairs. The
 799         supported parameter names along with their default values are shown below:
 800             
 801             RGroupCoreAlignment,MCS, RGroupMatching,GreedyChunks,chunkSize,5,
 802             matchOnlyAtRGroups,no,removeHydrogenOnlyGroups,yes,
 803             removeHydrogensPostMatch,no
 804             
 805         A brief description of each supported parameter taken from  RDKit documentation,
 806         along with their possible values, is as follows.
 807         
 808         RGroupCoreAlignment - Mapping of core labels:
 809         
 810             MCS - Map core labels to each other using MCS
 811             None - No mapping
 812         
 813         RGroupMatching: Greedy, GreedyChunks, Exhaustive
 814         
 815         matchOnlyAtRGroups - Allow R group decomposition only at specified R groups.
 816         Possible values: yes, no.
 817         
 818         removeHydrogenOnlyGroups - Remove all R groups that only have hydrogens.
 819         Possible values: yes, no.
 820         
 821         removeHydrogensPostMatch - Remove all hydrogens from the output molecules.
 822         Possible values: yes, no.
 823     -e, --examples
 824         Print examples.
 825     -h, --help
 826         Print this help message.
 827     -i, --infile <infile>
 828         Input file name.
 829     --infileParams <Name,Value,...>  [default: auto]
 830         A comma delimited list of parameter name and value pairs for reading 
 831         molecules from files. The supported parameter names for different file
 832         formats, along with their default values, are shown below:
 833             
 834             SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
 835             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 836                 smilesTitleLine,auto,sanitize,yes
 837             
 838         Possible values for smilesDelimiter: space, comma or tab.
 839     -m, --mcsParams <Name,Value,...>  [default: auto]
 840         Parameter values to use for identifying a maximum common substructure
 841         (MCS) in a series of molecules. In general, it is a comma delimited list of
 842         parameter name and value pairs. The supported parameter names along with
 843         their default values are shown below:
 844             
 845             atomCompare,CompareElements,bondCompare,CompareOrder,
 846             maximizeBonds,yes,matchValences,yes,matchChiralTag,no,
 847             minNumAtoms,1,minNumBonds,0,ringMatchesRingOnly,yes,
 848             completeRingsOnly,yes,threshold,1.0,timeOut,3600,seedSMARTS,none
 849             
 850         Possible values for atomCompare: CompareAny, CompareElements,
 851         CompareIsotopes. Possible values for bondCompare: CompareAny,
 852         CompareOrder, CompareOrderExact.
 853         
 854         A brief description of MCS parameters taken from RDKit documentation is
 855         as follows:
 856             
 857             atomCompare - Controls match between two atoms
 858             bondCompare - Controls match between two bonds
 859             maximizeBonds - Maximize number of bonds instead of atoms
 860             matchValences - Include atom valences in the MCS match
 861             matchChiralTag - Include atom chirality in the MCS match
 862             minNumAtoms - Minimum number of atoms in the MCS match
 863             minNumBonds - Minimum number of bonds in the MCS match
 864             ringMatchesRingOnly - Ring bonds only match other ring bonds
 865             completeRingsOnly - Partial rings not allowed during the match
 866             threshold - Fraction of the dataset that must contain the MCS
 867             seedSMARTS - SMARTS string as the seed of the MCS
 868             timeout - Timeout for the MCS calculation in seconds
 869             
 870     -o, --outfile <outfile>
 871         Output file name.
 872     --outfileParams <Name,Value,...>  [default: auto]
 873         A comma delimited list of parameter name and value pairs for writing
 874         molecules to files. The supported parameter names for different file
 875         formats, along with their default values, are shown below:
 876             
 877             SD: compute2DCoords,auto,kekulize,yes,forceV3000,no
 878             SMILES: smilesKekulize,no,smilesIsomeric,yes
 879             
 880         Default value for compute2DCoords: yes for SMILES input file; no for all other
 881         file types. The kekulize and smilesIsomeric parameters are also used during
 882         generation of SMILES strings for CSV/TSV files.
 883     --overwrite
 884         Overwrite existing files.
 885     -q, --quote <yes or no>  [default: auto]
 886         Quote SMILES strings and molecule names before writing them out to text
 887         files. Possible values: yes or no. Default: yes for CSV (.csv) text files; no for
 888         TSV (.tsv) and TXT (.txt) text files.
 889     -r, --removeUnmatched <yes or no>  [default: no]
 890         Remove unmatched molecules containing no specified core scaffold from the
 891         output file and write them to a different output file.
 892     -s, --smartsOrSmilesCoreScaffold <text>  [default: none]
 893         SMARTS or SMILES string to use for core scaffold during 'SMARTS' or 'SMILES'
 894         value of '-c, --coreScaffold' option. Multiple core scaffolds may be specified using a
 895         comma delimited set of SMARTS or SMILES strings.
 896     -w, --workingdir <dir>
 897         Location of working directory which defaults to the current directory.
 898 
 899 Examples:
 900     To perform R group decomposition for a set of molecules in a series using MCS
 901     to identify a core scaffold and write out a CSV file containing R groups, type:
 902 
 903         % RDKitPerformRGroupDecomposition.py -i SampleSeriesD3R.smi
 904           -o SampleSeriesD3ROut.csv
 905 
 906     To perform R group decomposition for a set of molecules in a series using a
 907     specified core scaffold and write out a SD file containing R groups, type:
 908 
 909         % RDKitPerformRGroupDecomposition.py  -c BySMARTS
 910           -s "Nc1nccc(-c2cnc(CNCc3ccccc3)c2)n1" -i SampleSeriesD3R.smi
 911           -o SampleSeriesD3ROut.sdf
 912 
 913     To perform R group decomposition for a set of molecules in a series using MCS
 914     to identify a core scaffold and write out CSV files containing matched and
 915     unmatched molecules without quoting values, type:
 916 
 917         % RDKitPerformRGroupDecomposition.py -c ByMCS -r yes -q no
 918           -i SampleSeriesD3R.sdf -o SampleSeriesD3ROut.csv
 919 
 920     To perform R group decomposition for a set of molecules in multiple series using
 921     specified core scaffolds and write out a TSV file containing R groups, type:
 922 
 923         % RDKitPerformRGroupDecomposition.py  -c BySMARTS
 924           -s "Nc1nccc(-c2cnc(CNCc3ccccc3)c2)n1,[#6]-[#6]1:[#6]:[#6]:[#6]:[#6]:
 925           [#6]:1" -i SampleMultipleSeriesD3R.smi -o
 926           SampleMultipleSeriesD3ROut.tsv
 927 
 928     To perform R group decomposition for a set of molecules in a CSV SMILES file,
 929     SMILES strings in  olumn 1, name in column 2, and write out a CSV file containing
 930     R groups, type:
 931      
 932         % RDKitPerformRGroupDecomposition.py --infileParams 
 933           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
 934           smilesNameColumn,2" --outfileParams "compute2DCoords,yes"
 935           -i SampleSeriesD3R.smi -o SampleSeriesD3ROut.csv
 936 
 937 Author:
 938     Manish Sud(msud@san.rr.com)
 939 
 940 See also:
 941     RDKitConvertFileFormat.py, RDKitSearchFunctionalGroups.py, RDKitSearchSMARTS.py
 942 
 943 Copyright:
 944     Copyright (C) 2026 Manish Sud. All rights reserved.
 945 
 946     The functionality available in this script is implemented using RDKit, an
 947     open source toolkit for cheminformatics developed by Greg Landrum.
 948 
 949     This file is part of MayaChemTools.
 950 
 951     MayaChemTools is free software; you can redistribute it and/or modify it under
 952     the terms of the GNU Lesser General Public License as published by the Free
 953     Software Foundation; either version 3 of the License, or (at your option) any
 954     later version.
 955 
 956 """
 957 
 958 if __name__ == "__main__":
 959     main()