MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: RDKitCalculateMolecularDescriptors.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 import numpy
  37 
  38 # RDKit imports...
  39 try:
  40     from rdkit import rdBase
  41     from rdkit import Chem
  42     from rdkit.Chem import AllChem
  43     from rdkit.Chem import rdMolDescriptors
  44     from rdkit.Chem import Descriptors
  45     from rdkit.Chem import Descriptors3D
  46 except ImportError as ErrMsg:
  47     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
  48     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
  49     sys.exit(1)
  50 
  51 # MayaChemTools imports...
  52 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  53 try:
  54     from docopt import docopt
  55     import MiscUtil
  56     import RDKitUtil
  57 except ImportError as ErrMsg:
  58     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  59     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  60     sys.exit(1)
  61 
  62 ScriptName = os.path.basename(sys.argv[0])
  63 Options = {}
  64 OptionsInfo = {}
  65 
  66 DescriptorNamesMap = {}
  67 
  68 
  69 def main():
  70     """Start execution of the script."""
  71 
  72     MiscUtil.PrintInfo(
  73         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
  74         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
  75     )
  76 
  77     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  78 
  79     # Retrieve command line arguments and options...
  80     RetrieveOptions()
  81 
  82     # Process and validate command line arguments and options...
  83     ProcessOptions()
  84 
  85     # Perform actions required by the script...
  86     CalculateMolecularDescriptors()
  87 
  88     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  89     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  90 
  91 
  92 def CalculateMolecularDescriptors():
  93     """Calculate molecular descriptors."""
  94 
  95     ProcessMolecularDescriptorsInfo()
  96     PerformCalculations()
  97 
  98 
  99 def ProcessMolecularDescriptorsInfo():
 100     """Process descriptors information."""
 101 
 102     RetrieveMolecularDescriptorsInfo()
 103     ProcessSpecifiedDescriptorNames()
 104 
 105 
 106 def PerformCalculations():
 107     """Calculate descriptors for a specified list of descriptors."""
 108 
 109     # Setup a molecule reader...
 110     MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
 111     Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
 112 
 113     # Setup a writer...
 114     Writer = SetupMoleculeWriter()
 115 
 116     # Process molecules...
 117     MolCount, ValidMolCount = ProcessMolecules(Mols, Writer)
 118 
 119     if Writer is not None:
 120         Writer.close()
 121 
 122     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
 123     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
 124     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
 125 
 126 
 127 def ProcessMolecules(Mols, Writer):
 128     """Process and filter molecules."""
 129 
 130     if OptionsInfo["MPMode"]:
 131         return ProcessMoleculesUsingMultipleProcesses(Mols, Writer)
 132     else:
 133         return ProcessMoleculesUsingSingleProcess(Mols, Writer)
 134 
 135 
 136 def ProcessMoleculesUsingSingleProcess(Mols, Writer):
 137     """Process molecules and calculate descriptors using a single process."""
 138 
 139     DescriptorsCount = len(OptionsInfo["SpecifiedDescriptorNames"])
 140     MiscUtil.PrintInfo(
 141         "\nCalculating %d molecular %s for each molecule..."
 142         % (DescriptorsCount, ("descroptors" if DescriptorsCount > 1 else "descriptor"))
 143     )
 144 
 145     (MolCount, ValidMolCount) = [0] * 2
 146     for MolIndex, Mol in enumerate(Mols):
 147         MolCount += 1
 148         if Mol is None:
 149             continue
 150 
 151         if RDKitUtil.IsMolEmpty(Mol):
 152             MolName = RDKitUtil.GetMolName(Mol, MolCount)
 153             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 154             continue
 155         ValidMolCount += 1
 156 
 157         # Calculate and write descriptor values...
 158         CalculatedValues = CalculateDescriptorValues(MolIndex, Mol)
 159         WriteDescriptorValues(Mol, MolCount, Writer, CalculatedValues)
 160 
 161     return (MolCount, ValidMolCount)
 162 
 163 
 164 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer):
 165     """Process molecules and calculate descriptors using multiprocessing."""
 166 
 167     DescriptorsCount = len(OptionsInfo["SpecifiedDescriptorNames"])
 168     MiscUtil.PrintInfo(
 169         "\nCalculating %d molecular %s for each molecule using multiprocessing......"
 170         % (DescriptorsCount, ("descroptors" if DescriptorsCount > 1 else "descriptor"))
 171     )
 172 
 173     MPParams = OptionsInfo["MPParams"]
 174 
 175     # Setup data for initializing a worker process...
 176     MiscUtil.PrintInfo("Encoding options info...")
 177     InitializeWorkerProcessArgs = (
 178         MiscUtil.ObjectToBase64EncodedString(Options),
 179         MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
 180     )
 181 
 182     # Setup a encoded mols data iterable for a worker process...
 183     WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
 184 
 185     # Setup process pool along with data initialization for each process...
 186     MiscUtil.PrintInfo(
 187         "\nConfiguring multiprocessing using %s method..."
 188         % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
 189     )
 190     MiscUtil.PrintInfo(
 191         "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
 192         % (
 193             MPParams["NumProcesses"],
 194             MPParams["InputDataMode"],
 195             ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
 196         )
 197     )
 198 
 199     ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
 200 
 201     # Start processing...
 202     if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
 203         Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
 204     elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
 205         Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
 206     else:
 207         MiscUtil.PrintError(
 208             'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
 209         )
 210 
 211     (MolCount, ValidMolCount) = [0] * 2
 212     for Result in Results:
 213         MolCount += 1
 214         MolIndex, EncodedMol, CalculatedValues = Result
 215 
 216         if EncodedMol is None:
 217             continue
 218         ValidMolCount += 1
 219 
 220         Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
 221 
 222         # Write descriptor values...
 223         WriteDescriptorValues(Mol, MolCount, Writer, CalculatedValues)
 224 
 225     return (MolCount, ValidMolCount)
 226 
 227 
 228 def InitializeWorkerProcess(*EncodedArgs):
 229     """Initialize data for a worker process."""
 230 
 231     global Options, OptionsInfo
 232 
 233     MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
 234 
 235     # Decode Options and OptionInfo...
 236     Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
 237     OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
 238 
 239     RetrieveMolecularDescriptorsInfo(PrintInfo=False)
 240 
 241 
 242 def WorkerProcess(EncodedMolInfo):
 243     """Process data for a worker process."""
 244 
 245     MolIndex, EncodedMol = EncodedMolInfo
 246 
 247     CalculatedValues = []
 248     if EncodedMol is None:
 249         return [MolIndex, None, CalculatedValues]
 250 
 251     Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
 252     if RDKitUtil.IsMolEmpty(Mol):
 253         MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
 254         MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 255         return [MolIndex, None, CalculatedValues]
 256 
 257     return [MolIndex, EncodedMol, CalculateDescriptorValues(MolIndex, Mol)]
 258 
 259 
 260 def CalculateDescriptorValues(MolIndex, Mol):
 261     """Calculate descriptor values."""
 262 
 263     return [
 264         FormatCalculatedValue(CalculateDescriptorValue(MolIndex, Mol, Name), OptionsInfo["Precision"])
 265         for Name in OptionsInfo["SpecifiedDescriptorNames"]
 266     ]
 267 
 268 
 269 def CalculateDescriptorValue(MolIndex, Mol, Name):
 270     """Calculate value for a specific descriptor along with handling any calculation failure."""
 271 
 272     try:
 273         Value = DescriptorNamesMap["ComputeFunction"][Name](Mol)
 274     except ValueError as ErrMsg:
 275         MiscUtil.PrintWarning(
 276             "Failed to calculate descriptor %s for molecule %s:\n%s\n" % (Name, (MolIndex + 1), ErrMsg)
 277         )
 278         Value = "NA"
 279 
 280     return Value
 281 
 282 
 283 def WriteDescriptorValues(Mol, MolNum, Writer, CalculatedValues):
 284     """Write out calculated descriptor values."""
 285 
 286     if OptionsInfo["TextOutFileMode"]:
 287         LineWords = []
 288         if OptionsInfo["SMILESOut"]:
 289             SMILES = Chem.MolToSmiles(Mol, isomericSmiles=True, canonical=True)
 290             LineWords.append(SMILES)
 291 
 292         MolName = RDKitUtil.GetMolName(Mol, MolNum)
 293         LineWords.append(MolName)
 294         LineWords.extend(CalculatedValues)
 295         Line = OptionsInfo["TextOutFileDelim"].join(LineWords)
 296         Writer.write("%s\n" % Line)
 297     else:
 298         for Index, Name in enumerate(OptionsInfo["SpecifiedDescriptorNames"]):
 299             Mol.SetProp(Name, CalculatedValues[Index])
 300 
 301         if OptionsInfo["OutfileParams"]["Compute2DCoords"]:
 302             AllChem.Compute2DCoords(Mol)
 303 
 304         Writer.write(Mol)
 305 
 306 
 307 def SetupMoleculeWriter():
 308     """Setup a molecule writer."""
 309 
 310     if OptionsInfo["TextOutFileMode"]:
 311         Writer = open(OptionsInfo["Outfile"], "w")
 312     else:
 313         Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
 314 
 315     if Writer is None:
 316         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
 317 
 318     MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"])
 319 
 320     # Wite out headers for a text file...
 321     if OptionsInfo["TextOutFileMode"]:
 322         LineWords = []
 323         if OptionsInfo["SMILESOut"]:
 324             LineWords.append("SMILES")
 325         LineWords.append("MolID")
 326         LineWords.extend(OptionsInfo["SpecifiedDescriptorNames"])
 327         Line = OptionsInfo["TextOutFileDelim"].join(LineWords)
 328         Writer.write("%s\n" % Line)
 329 
 330     return Writer
 331 
 332 
 333 def FormatCalculatedValue(Value, Precision):
 334     """Format calculated value of descriptor based on its type."""
 335 
 336     if (type(Value) is float) or (type(Value) is numpy.float64):
 337         FormattedValue = "%.*f" % (Precision, Value)
 338         if not re.search("[1-9]", FormattedValue):
 339             FormattedValue = "0.0"
 340     elif type(Value) is list:
 341         FormattedValue = "%s" % Value
 342         FormattedValue = re.sub(r"\[|\]|,", "", FormattedValue)
 343     else:
 344         FormattedValue = "%s" % Value
 345 
 346     return FormattedValue
 347 
 348 
 349 def ProcessSpecifiedDescriptorNames():
 350     """Process and validate specified decriptor names."""
 351 
 352     OptionsInfo["SpecifiedDescriptorNames"] = []
 353 
 354     if not re.match("^(2D|3D|All|FragmentCountOnly|Specify)$", OptionsInfo["Mode"], re.I):
 355         MiscUtil.PrintError('Mode value, %s, using "-m, --mode" option is not a valid value.' % OptionsInfo["Mode"])
 356 
 357     if re.match("^2D$", OptionsInfo["Mode"], re.I):
 358         OptionsInfo["SpecifiedDescriptorNames"] = DescriptorNamesMap["2D"]["Names"]
 359         if OptionsInfo["FragmentCount"]:
 360             OptionsInfo["SpecifiedDescriptorNames"].extend(DescriptorNamesMap["FragmentCount"]["Names"])
 361         return
 362     elif re.match("^3D$", OptionsInfo["Mode"], re.I):
 363         OptionsInfo["SpecifiedDescriptorNames"] = DescriptorNamesMap["3D"]["Names"]
 364         return
 365     elif re.match("^All$", OptionsInfo["Mode"], re.I):
 366         OptionsInfo["SpecifiedDescriptorNames"] = DescriptorNamesMap["2D"]["Names"]
 367         if OptionsInfo["FragmentCount"]:
 368             OptionsInfo["SpecifiedDescriptorNames"].extend(DescriptorNamesMap["FragmentCount"]["Names"])
 369         OptionsInfo["SpecifiedDescriptorNames"].extend(DescriptorNamesMap["3D"]["Names"])
 370         return
 371     elif re.match("^FragmentCountOnly$", OptionsInfo["Mode"], re.I):
 372         OptionsInfo["SpecifiedDescriptorNames"] = DescriptorNamesMap["FragmentCount"]["Names"]
 373         return
 374 
 375     # Set up a canonical descriptor names map for checking specified names...
 376     CanonicalNameMap = {}
 377     for Name in DescriptorNamesMap["ComputeFunction"]:
 378         CanonicalNameMap[Name.lower()] = Name
 379 
 380     # Parse and validate specified names...
 381     DescriptorNames = re.sub(" ", "", OptionsInfo["DescriptorNames"])
 382     if not DescriptorNames:
 383         MiscUtil.PrintError('No descriptor names specified for "-d, --descriptorNames" option')
 384 
 385     SMILESInfile = MiscUtil.CheckFileExt(Options["--infile"], "smi")
 386     Canonical3DNameMap = {}
 387     if SMILESInfile:
 388         for Name in DescriptorNamesMap["3D"]["Names"]:
 389             Canonical3DNameMap[Name.lower()] = Name
 390 
 391     SpecifiedDescriptorNames = []
 392     for Name in DescriptorNames.split(","):
 393         CanonicalName = Name.lower()
 394         if CanonicalName in CanonicalNameMap:
 395             SpecifiedDescriptorNames.append(CanonicalNameMap[CanonicalName])
 396         else:
 397             MiscUtil.PrintError(
 398                 'The descriptor name, %s, specified using "-d, --descriptorNames" option is not a valid name.' % (Name)
 399             )
 400         if SMILESInfile:
 401             if CanonicalName in Canonical3DNameMap:
 402                 MiscUtil.PrintError(
 403                     'The 3D descriptor name, %s, specified using "-d, --descriptorNames" option is not a valid for SMILES input file.'
 404                     % (Name)
 405                 )
 406 
 407     if not len(SpecifiedDescriptorNames):
 408         MiscUtil.PrintError('No valid descriptor name specified for "-d, --descriptorNames" option')
 409 
 410     OptionsInfo["SpecifiedDescriptorNames"] = SpecifiedDescriptorNames
 411 
 412 
 413 def RetrieveMolecularDescriptorsInfo(PrintInfo=True):
 414     """Retrieve descriptors information."""
 415 
 416     if PrintInfo:
 417         MiscUtil.PrintInfo("\nRetrieving information for avalible molecular descriptors...")
 418 
 419     # Initialze data for 2D, FragmentCount and 3D descriptors...
 420     DescriptorNamesMap["Types"] = ["2D", "FragmentCount", "3D"]
 421     DescriptorNamesMap["ComputeFunction"] = {}
 422 
 423     Autocorr2DExclude = OptionsInfo["Autocorr2DExclude"] if "Autocorr2DExclude" in OptionsInfo else False
 424 
 425     for Type in DescriptorNamesMap["Types"]:
 426         DescriptorNamesMap[Type] = {}
 427         DescriptorNamesMap[Type]["Names"] = []
 428 
 429     # Setup data for 2D and FragmentCount...
 430     DescriptorsInfo = Descriptors.descList
 431     for DescriptorInfo in DescriptorsInfo:
 432         Name = DescriptorInfo[0]
 433         ComputeFunction = DescriptorInfo[1]
 434 
 435         Type = "2D"
 436         if re.match("^fr_", Name, re.I):
 437             Type = "FragmentCount"
 438         elif re.match("^Autocorr2D$", Name, re.I):
 439             if Autocorr2DExclude:
 440                 continue
 441 
 442         if Name in DescriptorNamesMap["ComputeFunction"]:
 443             if PrintInfo:
 444                 MiscUtil.PrintWarning("Ignoring duplicate descriptor name: %s..." % Name)
 445         else:
 446             DescriptorNamesMap[Type]["Names"].append(Name)
 447             DescriptorNamesMap["ComputeFunction"][Name] = ComputeFunction
 448 
 449     # Add new 2D decriptor name to the list...
 450     Type = "2D"
 451     if not Autocorr2DExclude:
 452         try:
 453             Name = "Autocorr2D"
 454             ComputeFunction = rdMolDescriptors.CalcAUTOCORR2D
 455             if Name not in DescriptorNamesMap["ComputeFunction"]:
 456                 DescriptorNamesMap[Type]["Names"].append(Name)
 457                 DescriptorNamesMap["ComputeFunction"][Name] = ComputeFunction
 458         except AttributeError:
 459             if PrintInfo:
 460                 MiscUtil.PrintInfo("2D descriptor, %s, is not available in your current version of RDKit." % Name)
 461 
 462     # Set data for 3D descriptors...
 463     Type = "3D"
 464     NameToComputeFunctionMap = {
 465         "PMI1": Descriptors3D.PMI1,
 466         "PMI2": Descriptors3D.PMI2,
 467         "PMI3": Descriptors3D.PMI3,
 468         "NPR1": Descriptors3D.NPR1,
 469         "NPR2": Descriptors3D.NPR2,
 470         "RadiusOfGyration": Descriptors3D.RadiusOfGyration,
 471         "InertialShapeFactor": Descriptors3D.InertialShapeFactor,
 472         "Eccentricity": Descriptors3D.Eccentricity,
 473         "Asphericity": Descriptors3D.Asphericity,
 474         "SpherocityIndex": Descriptors3D.SpherocityIndex,
 475     }
 476 
 477     for Name in NameToComputeFunctionMap:
 478         ComputeFunction = NameToComputeFunctionMap[Name]
 479         if Name in DescriptorNamesMap["ComputeFunction"]:
 480             if PrintInfo:
 481                 MiscUtil.PrintWarning("Ignoring duplicate descriptor name: %s..." % Name)
 482         else:
 483             DescriptorNamesMap[Type]["Names"].append(Name)
 484             DescriptorNamesMap["ComputeFunction"][Name] = ComputeFunction
 485 
 486     # Check and add new 3D descriptors not directly available through Descriptors3D module...
 487     Type = "3D"
 488     AvailableName3DMap = {}
 489     for Name in ["Autocorr3D", "RDF", "MORSE", "WHIM", "GETAWAY"]:
 490         ComputeFunction = None
 491         try:
 492             if re.match("^Autocorr3D$", Name, re.I):
 493                 ComputeFunction = rdMolDescriptors.CalcAUTOCORR3D
 494             elif re.match("^RDF$", Name, re.I):
 495                 ComputeFunction = rdMolDescriptors.CalcRDF
 496             elif re.match("^MORSE$", Name, re.I):
 497                 ComputeFunction = rdMolDescriptors.CalcMORSE
 498             elif re.match("^WHIM$", Name, re.I):
 499                 ComputeFunction = rdMolDescriptors.CalcWHIM
 500             elif re.match("^GETAWAY$", Name, re.I):
 501                 ComputeFunction = rdMolDescriptors.CalcGETAWAY
 502             else:
 503                 ComputeFunction = None
 504         except AttributeError:
 505             if PrintInfo:
 506                 MiscUtil.PrintWarning("3D descriptor, %s, is not available in your current version of RDKit" % Name)
 507 
 508         if ComputeFunction is not None:
 509             AvailableName3DMap[Name] = ComputeFunction
 510 
 511     for Name in AvailableName3DMap:
 512         ComputeFunction = AvailableName3DMap[Name]
 513         if Name not in DescriptorNamesMap["ComputeFunction"]:
 514             DescriptorNamesMap[Type]["Names"].append(Name)
 515             DescriptorNamesMap["ComputeFunction"][Name] = ComputeFunction
 516 
 517     Count = 0
 518     TypesCount = []
 519     for Type in DescriptorNamesMap["Types"]:
 520         TypeCount = len(DescriptorNamesMap[Type]["Names"])
 521         TypesCount.append(TypeCount)
 522         Count += TypeCount
 523 
 524     if not Count:
 525         if PrintInfo:
 526             MiscUtil.PrintError("Failed to retrieve any  molecular descriptors...")
 527 
 528     # Sort descriptor names...
 529     for Type in DescriptorNamesMap["Types"]:
 530         DescriptorNamesMap[Type]["Names"] = sorted(DescriptorNamesMap[Type]["Names"])
 531 
 532     if PrintInfo:
 533         MiscUtil.PrintInfo("\nTotal number of availble molecular descriptors: %d" % Count)
 534     for Index in range(0, len(DescriptorNamesMap["Types"])):
 535         Type = DescriptorNamesMap["Types"][Index]
 536         TypeCount = TypesCount[Index]
 537         if PrintInfo:
 538             MiscUtil.PrintInfo("Number of %s molecular descriptors: %d" % (Type, TypeCount))
 539 
 540 
 541 def ListMolecularDescriptorsInfo():
 542     """List descriptors information."""
 543 
 544     MiscUtil.PrintInfo("\nListing information for avalible molecular descriptors...")
 545 
 546     Delimiter = ", "
 547     for Type in DescriptorNamesMap["Types"]:
 548         Names = DescriptorNamesMap[Type]["Names"]
 549         MiscUtil.PrintInfo("\n%s descriptors: %s" % (Type, Delimiter.join(Names)))
 550 
 551     MiscUtil.PrintInfo("")
 552 
 553 
 554 def ProcessOptions():
 555     """Process and validate command line arguments and options."""
 556 
 557     MiscUtil.PrintInfo("Processing options...")
 558 
 559     # Validate options...
 560     ValidateOptions()
 561 
 562     OptionsInfo["Autocorr2DExclude"] = True
 563     if not re.match("^Yes$", Options["--autocorr2DExclude"], re.I):
 564         OptionsInfo["Autocorr2DExclude"] = False
 565 
 566     OptionsInfo["FragmentCount"] = True
 567     if not re.match("^Yes$", Options["--fragmentCount"], re.I):
 568         OptionsInfo["FragmentCount"] = False
 569 
 570     OptionsInfo["DescriptorNames"] = Options["--descriptorNames"]
 571     OptionsInfo["Mode"] = Options["--mode"]
 572 
 573     OptionsInfo["Infile"] = Options["--infile"]
 574     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 575         "--infileParams", Options["--infileParams"], Options["--infile"]
 576     )
 577 
 578     OptionsInfo["Outfile"] = Options["--outfile"]
 579     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 580         "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"]
 581     )
 582 
 583     OptionsInfo["Overwrite"] = Options["--overwrite"]
 584 
 585     TextOutFileMode = False
 586     TextOutFileDelim = ""
 587     if MiscUtil.CheckFileExt(Options["--outfile"], "csv"):
 588         TextOutFileMode = True
 589         TextOutFileDelim = ","
 590     elif MiscUtil.CheckFileExt(Options["--outfile"], "tsv txt"):
 591         TextOutFileMode = True
 592         TextOutFileDelim = "\t"
 593     OptionsInfo["TextOutFileMode"] = TextOutFileMode
 594     OptionsInfo["TextOutFileDelim"] = TextOutFileDelim
 595 
 596     OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
 597     OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
 598 
 599     OptionsInfo["Precision"] = int(Options["--precision"])
 600 
 601     OptionsInfo["SMILESOut"] = False
 602     if re.match("^Yes$", Options["--smilesOut"], re.I):
 603         OptionsInfo["SMILESOut"] = True
 604 
 605 
 606 def RetrieveOptions():
 607     """Retrieve command line arguments and options."""
 608 
 609     # Get options...
 610     global Options
 611     Options = docopt(_docoptUsage_)
 612 
 613     # Set current working directory to the specified directory...
 614     WorkingDir = Options["--workingdir"]
 615     if WorkingDir:
 616         os.chdir(WorkingDir)
 617 
 618     # Handle examples option...
 619     if "--examples" in Options and Options["--examples"]:
 620         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 621         sys.exit(0)
 622 
 623     # Handle listing of descriptor information...
 624     if Options["--list"]:
 625         ProcessListMolecularDescriptorsOption()
 626         sys.exit(0)
 627 
 628 
 629 def ProcessListMolecularDescriptorsOption():
 630     """Process list descriptors information."""
 631 
 632     RetrieveMolecularDescriptorsInfo()
 633     ListMolecularDescriptorsInfo()
 634 
 635 
 636 def ValidateOptions():
 637     """Validate option values."""
 638 
 639     MiscUtil.ValidateOptionTextValue("-a, --autocorr2DExclude", Options["--autocorr2DExclude"], "yes no")
 640     MiscUtil.ValidateOptionTextValue("-f, --fragmentCount", Options["--fragmentCount"], "yes no")
 641 
 642     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "2D 3D All FragmentCountOnly Specify")
 643 
 644     if re.match("^Specify$", Options["--mode"], re.I):
 645         if re.match("^none$", Options["--descriptorNames"], re.I):
 646             MiscUtil.PrintError(
 647                 'The name(s) of molecular descriptors must be specified using "-d, --descriptorNames" option during "Specify" value of "-m, --mode" option.'
 648             )
 649 
 650     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 651     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi csv tsv txt")
 652 
 653     if re.match("^3D|All$", Options["--mode"], re.I):
 654         if MiscUtil.CheckFileExt(Options["--infile"], "smi"):
 655             MiscUtil.PrintError(
 656                 'The input SMILES file, %s, is not valid for  "3D or All" value of "-m, --mode" option.'
 657             )
 658 
 659     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd csv tsv txt")
 660     MiscUtil.ValidateOptionsOutputFileOverwrite(
 661         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 662     )
 663     MiscUtil.ValidateOptionsDistinctFileNames(
 664         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 665     )
 666 
 667     MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
 668 
 669     MiscUtil.ValidateOptionIntegerValue("-p, --precision", Options["--precision"], {">": 0})
 670     MiscUtil.ValidateOptionTextValue("-s, --smilesOut", Options["--smilesOut"], "yes no")
 671 
 672 
 673 # Setup a usage string for docopt...
 674 _docoptUsage_ = """
 675 RDKitCalculateMolecularDescriptors.py - Calculate 2D/3D molecular descriptors
 676 
 677 Usage:
 678     RDKitCalculateMolecularDescriptors.py [--autocorr2DExclude <yes or no>] [--fragmentCount <yes or no>]
 679                                           [--descriptorNames <Name1,Name2,...>] [--infileParams <Name,Value,...>]
 680                                           [--mode <2D, 3D, All...>] [--mp <yes or no>] [--mpParams <Name,Value,...>]
 681                                           [--outfileParams <Name,Value,...>] [--overwrite] [--precision <number>]
 682                                           [--smilesOut <yes or no>] [-w <dir>] -i <infile> -o <outfile> 
 683     RDKitCalculateMolecularDescriptors.py -l | --list
 684     RDKitCalculateMolecularDescriptors.py -h | --help | -e | --examples
 685 
 686 Description:
 687     Calculate 2D/3D molecular descriptors for molecules and write them out to a SD or
 688     CSV/TSV text file.
 689 
 690     The complete list of currently available molecular descriptors may be obtained by
 691     using '-l, --list' option. The names of valid 2D, fragment count, and 3D molecular
 692     descriptors are shown below:
 693 
 694     2D descriptors: Autocorr2D, AvgIpc, BCUT2D_*, BalabanJ, BertzCT, Chi0, Chi1, Chi0n - Chi4n,
 695     Chi0v - Chi4v, EState_VSA1 - EState_VSA11, ExactMolWt, FpDensityMorgan1, FpDensityMorgan2,
 696     FpDensityMorgan3, FractionCSP3, HallKierAlpha, HeavyAtomCount, HeavyAtomMolWt, Ipc,
 697     Kappa1 - Kappa3, LabuteASA, MaxAbsEStateIndex, MaxAbsPartialCharge, MaxEStateIndex,
 698     MaxPartialCharge, MinAbsEStateIndex, MinAbsPartialCharge, MinEStateIndex, MinPartialCharge,
 699     MolLogP, MolMR, MolWt, NHOHCount, NOCount, NumAliphaticCarbocycles, NumAliphaticHeterocycles,
 700     NumAliphaticRings, NumAromaticCarbocycles, NumAromaticHeterocycles, NumAromaticRings,
 701     NumHAcceptors, NumHDonors, NumHeteroatoms, NumRadicalElectrons, NumRotatableBonds,
 702     NumSaturatedCarbocycles, NumSaturatedHeterocycles, NumSaturatedRings, NumValenceElectrons,
 703     PEOE_VSA1 - PEOE_VSA14,  RingCount, SMR_VSA1 - SMR_VSA10, SlogP_VSA1 - SlogP_VSA12,
 704     TPSA, VSA_EState1 - VSA_EState10, qed
 705 
 706     FragmentCount 2D descriptors: fr_Al_COO, fr_Al_OH, fr_Al_OH_noTert, fr_ArN, fr_Ar_COO,
 707     fr_Ar_N, fr_Ar_NH, fr_Ar_OH, fr_COO, fr_COO2, fr_C_O, fr_C_O_noCOO, fr_C_S, fr_HOCCN,
 708     fr_Imine, fr_NH0, fr_NH1, fr_NH2, fr_N_O, fr_Ndealkylation1, fr_Ndealkylation2, fr_Nhpyrrole,
 709     fr_SH, fr_aldehyde, fr_alkyl_carbamate, fr_alkyl_halide, fr_allylic_oxid, fr_amide, fr_amidine,
 710     fr_aniline, fr_aryl_methyl, fr_azide, fr_azo, fr_barbitur, fr_benzene, fr_benzodiazepine,
 711     fr_bicyclic, fr_diazo, fr_dihydropyridine, fr_epoxide, fr_ester, fr_ether, fr_furan, fr_guanido,
 712     fr_halogen, fr_hdrzine, fr_hdrzone, fr_imidazole, fr_imide, fr_isocyan, fr_isothiocyan, fr_ketone,
 713     fr_ketone_Topliss, fr_lactam, fr_lactone, fr_methoxy, fr_morpholine, fr_nitrile, fr_nitro,
 714     fr_nitro_arom, fr_nitro_arom_nonortho, fr_nitroso, fr_oxazole, fr_oxime, fr_para_hydroxylation,
 715     fr_phenol, fr_phenol_noOrthoHbond, fr_phos_acid, fr_phos_ester, fr_piperdine, fr_piperzine,
 716     fr_priamide, fr_prisulfonamd, fr_pyridine, fr_quatN, fr_sulfide, fr_sulfonamd, fr_sulfone,
 717     fr_term_acetylene, fr_tetrazole, fr_thiazole, fr_thiocyan, fr_thiophene, fr_unbrch_alkane, fr_urea
 718 
 719     3D descriptors: Asphericity, Autocorr3D, Eccentricity, GETAWAY, InertialShapeFactor, MORSE,
 720     NPR1, NPR2, PMI1, PMI2, PMI3, RDF, RadiusOfGyration, SpherocityIndex, WHIM
 721 
 722     The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
 723     .txt, .csv, .tsv)
 724 
 725     The supported output file formats are: SD File (.sdf, .sd), CSV/TSV (.csv, .tsv, .txt)
 726 
 727 Options:
 728     -a, --autocorr2DExclude <yes or no>  [default: yes]
 729         Exclude Autocorr2D descriptor from the calculation of 2D descriptors. 
 730     -f, --fragmentCount <yes or no>  [default: yes]
 731         Include 2D fragment count descriptors during the calculation. These descriptors are
 732         counted using SMARTS patterns specified in FragmentDescriptors.csv file distributed
 733         with RDKit. This option is only used during '2D' or 'All' value of '-m, --mode' option.
 734     -d, --descriptorNames <Name1,Name2,...>  [default: none]
 735         A comma delimited list of supported molecular descriptor names to calculate.
 736         This option is only used during 'Specify' value of '-m, --mode' option.
 737     -e, --examples
 738         Print examples.
 739     -h, --help
 740         Print this help message.
 741     -i, --infile <infile>
 742         Input file name.
 743     --infileParams <Name,Value,...>  [default: auto]
 744         A comma delimited list of parameter name and value pairs for reading
 745         molecules from files. The supported parameter names for different file
 746         formats, along with their default values, are shown below:
 747             
 748             SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
 749             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 750                 smilesTitleLine,auto,sanitize,yes
 751             
 752         Possible values for smilesDelimiter: space, comma or tab.
 753     -l, --list
 754         List molecular descriptors without performing any calculations.
 755     -m, --mode <2D, 3D, All, FragmentCountOnly, or Specify>  [default: 2D]
 756         Type of molecular descriptors to calculate. Possible values: 2D, 3D,
 757         All or Specify. The name of molecular descriptors must be specified using
 758         '-d, --descriptorNames' for 'Specify'. 2D descriptors also include 1D descriptors.
 759         The structure  of molecules must contain 3D coordinates for the  calculation
 760         of 3D descriptors.
 761     --mp <yes or no>  [default: no]
 762         Use multiprocessing.
 763          
 764         By default, input data is retrieved in a lazy manner via mp.Pool.imap()
 765         function employing lazy RDKit data iterable. This allows processing of
 766         arbitrary large data sets without any additional requirements memory.
 767         
 768         All input data may be optionally loaded into memory by mp.Pool.map()
 769         before starting worker processes in a process pool by setting the value
 770         of 'inputDataMode' to 'InMemory' in '--mpParams' option.
 771         
 772         A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
 773         data mode may adversely impact the performance. The '--mpParams' section
 774         provides additional information to tune the value of 'chunkSize'.
 775     --mpParams <Name,Value,...>  [default: auto]
 776         A comma delimited list of parameter name and value pairs to configure
 777         multiprocessing.
 778         
 779         The supported parameter names along with their default and possible
 780         values are shown below:
 781         
 782             chunkSize, auto
 783             inputDataMode, Lazy   [ Possible values: InMemory or Lazy ]
 784             numProcesses, auto   [ Default: mp.cpu_count() ]
 785         
 786         These parameters are used by the following functions to configure and
 787         control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
 788         mp.Pool.imap().
 789         
 790         The chunkSize determines chunks of input data passed to each worker
 791         process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
 792         The default value of chunkSize is dependent on the value of 'inputDataMode'.
 793         
 794         The mp.Pool.map() function, invoked during 'InMemory' input data mode,
 795         automatically converts RDKit data iterable into a list, loads all data into
 796         memory, and calculates the default chunkSize using the following method
 797         as shown in its code:
 798         
 799             chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
 800             if extra: chunkSize += 1
 801         
 802         For example, the default chunkSize will be 7 for a pool of 4 worker processes
 803         and 100 data items.
 804         
 805         The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
 806         'lazy' RDKit data iterable to retrieve data as needed, without loading all the
 807         data into memory. Consequently, the size of input data is not known a priori.
 808         It's not possible to estimate an optimal value for the chunkSize. The default 
 809         chunkSize is set to 1.
 810         
 811         The default value for the chunkSize during 'Lazy' data mode may adversely
 812         impact the performance due to the overhead associated with exchanging
 813         small chunks of data. It is generally a good idea to explicitly set chunkSize to
 814         a larger value during 'Lazy' input data mode, based on the size of your input
 815         data and number of processes in the process pool.
 816         
 817         The mp.Pool.map() function waits for all worker processes to process all
 818         the data and return the results. The mp.Pool.imap() function, however,
 819         returns the the results obtained from worker processes as soon as the
 820         results become available for specified chunks of data.
 821         
 822         The order of data in the results returned by both mp.Pool.map() and 
 823         mp.Pool.imap() functions always corresponds to the input data.
 824     -o, --outfile <outfile>
 825         Output file name.
 826     --outfileParams <Name,Value,...>  [default: auto]
 827         A comma delimited list of parameter name and value pairs for writing
 828         molecules to files. The supported parameter names for different file
 829         formats, along with their default values, are shown below:
 830             
 831             SD: compute2DCoords,auto,kekulize,yes,forceV3000,no
 832             
 833         Default value for compute2DCoords: yes for SMILES input file; no for all other
 834         file types.
 835     -p, --precision <number>  [default: 3]
 836         Floating point precision for writing the calculated descriptor values.
 837     -s, --smilesOut <yes or no>  [default: no]
 838         Write out SMILES string to CSV/TSV text output file.
 839     --overwrite
 840         Overwrite existing files.
 841     -w, --workingdir <dir>
 842         Location of working directory which defaults to the current directory.
 843 
 844 Examples:
 845     To compute all available 2D descriptors except Autocorr2D descriptor and
 846     write out a CSV file, type:
 847 
 848         % RDKitCalculateMolecularDescriptors.py  -i Sample.smi -o SampleOut.csv
 849 
 850     To compute all available 2D descriptors except Autocorr2D descriptor in
 851     multiprocessing mode on all available CPUs without loading all data into
 852     memory, and write out a CSV file, type:
 853 
 854         % RDKitCalculateMolecularDescriptors.py  --mp yes -i Sample.smi
 855           -o SampleOut.csv
 856 
 857     To compute all available 2D descriptors except Autocorr2D descriptor in
 858     multiprocessing mode on all available CPUs by loading all data into memory,
 859     and write out a CSV file, type:
 860 
 861         % RDKitCalculateMolecularDescriptors.py  --mp yes --mpParams
 862           "inputDataMode,InMemory" -i Sample.smi -o SampleOut.csv
 863 
 864     To compute all available 2D descriptors except Autocorr2D descriptor in
 865     multiprocessing mode on specific number of CPUs and chunk size without
 866     loading all data into memory, and write out a SDF file, type:
 867 
 868         % RDKitCalculateMolecularDescriptors.py  --mp yes --mpParams
 869           "inputDataMode,Lazy,numProcesses,4,chunkSize,8" -i Sample.smi
 870           -o SampleOut.sdf
 871 
 872     To compute all available 2D descriptors including Autocorr2D descriptor and
 873     excluding fragment count descriptors, and write out a TSV file, type:
 874 
 875         % RDKitCalculateMolecularDescriptors.py  -m 2D -a no -f no
 876           -i Sample.smi -o SampleOut.tsv
 877 
 878     To compute all available 3D descriptors and write out a SD file, type:
 879 
 880         % RDKitCalculateMolecularDescriptors.py  -m 3D -i Sample3D.sdf
 881           -o Sample3DOut.sdf
 882 
 883     To compute only fragment count 2D descriptors and write out a SD
 884     file file, type:
 885 
 886         % RDKitCalculateMolecularDescriptors.py  -m FragmentCountOnly
 887           -i Sample.sdf -o SampleOut.sdf
 888 
 889     To compute all available 2D and 3D descriptors including fragment count and
 890     Autocorr2D and write out a CSV file, type:
 891 
 892         % RDKitCalculateMolecularDescriptors.py  -m All -a no -i Sample.sdf
 893           -o SampleOut.csv
 894 
 895     To compute a specific set of 2D and 3D descriptors and write out a
 896     write out a TSV file, type:
 897 
 898         % RDKitCalculateMolecularDescriptors.py  -m specify
 899           -d 'MolWt,MolLogP,NHOHCount, NOCount,RadiusOfGyration'
 900           -i Sample3D.sdf -o SampleOut.csv
 901 
 902     To compute all available 2D descriptors except Autocorr2D descriptor for 
 903     molecules in a CSV SMILES file, SMILES strings in column 1, name in
 904     column 2, and write out a SD file without calculation of 2D coordinates, type:
 905 
 906         % RDKitCalculateMolecularDescriptors.py --infileParams
 907           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
 908           smilesNameColumn,2" --outfileParams "compute2DCoords,no"
 909           -i SampleSMILES.csv -o SampleOut.sdf
 910 
 911 Author:
 912     Manish Sud(msud@san.rr.com)
 913 
 914 See also:
 915     RDKitCalculateRMSD.py, RDKitCompareMoleculeShapes.py, RDKitConvertFileFormat.py,
 916     RDKitGenerateConformers.py, RDKitPerformMinimization.py
 917 
 918 Copyright:
 919     Copyright (C) 2026 Manish Sud. All rights reserved.
 920 
 921     The functionality available in this script is implemented using RDKit, an
 922     open source toolkit for cheminformatics developed by Greg Landrum.
 923 
 924     This file is part of MayaChemTools.
 925 
 926     MayaChemTools is free software; you can redistribute it and/or modify it under
 927     the terms of the GNU Lesser General Public License as published by the Free
 928     Software Foundation; either version 3 of the License, or (at your option) any
 929     later version.
 930 
 931 """
 932 
 933 if __name__ == "__main__":
 934     main()