       1 #!/bin/env python
       2 #
       3 # File: RDKitPerformMinimization.py
       4 # Author: Manish Sud <msud@san.rr.com>
       5 #
       6 # Copyright (C) 2018 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 # Add local python path to the global path and import standard library modules...
      32 import os
      33 import sys;  sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
      34 import time
      35 import re
      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 Descriptors
      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 try:
      50     from docopt import docopt
      51     import MiscUtil
      52     import RDKitUtil
      53 except ImportError as ErrMsg:
      54     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
      55     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
      56     sys.exit(1)
      57
      58 ScriptName = os.path.basename(sys.argv[0])
      59 Options = {}
      60 OptionsInfo = {}
      61
      62 def main():
      63     """Start execution of the script"""
      64
      65     MiscUtil.PrintInfo("\n%s (RDK v%s; %s): Starting...\n" % (ScriptName, rdBase.rdkitVersion, time.asctime()))
      66
      67     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
      68
      69     # Retrieve command line arguments and options...
      70     RetrieveOptions()
      71
      72     # Process and validate command line arguments and options...
      73     ProcessOptions()
      74
      75     # Perform actions required by the script...
      76     PerformMinimization()
      77
      78     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
      79     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
      80
      81 def PerformMinimization():
      82     """Perform minimization."""
      83
      84     Infile = OptionsInfo["Infile"]
      85     Outfile = OptionsInfo["Outfile"]
      86
      87     # Setup a molecule reader...
      88     MiscUtil.PrintInfo("\nProcessing file %s..." % Infile)
      89     Mols  = RDKitUtil.ReadMolecules(Infile, **OptionsInfo["InfileParams"])
      90
      91     # Set up a molecule writer...
      92     Writer = None
      93     Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
      94     if Writer is None:
      95         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
      96     MiscUtil.PrintInfo("Generating file %s...\n" % Outfile)
      97
      98     # Process molecules...
      99     MolCount = 0
     100     ValidMolCount = 0
     101     MinimizationFailedCount = 0
     102
     103     if OptionsInfo["SkipConformerGeneration"]:
     104         MiscUtil.PrintInfo("Performing minimization without generation of conformers...\n")
     105     else:
     106         MiscUtil.PrintInfo("Performing minimization with generation of conformers...\n")
     107
     108     for Mol in Mols:
     109         MolCount += 1
     110
     111         if Mol is None:
     112             continue
     113
     114         if RDKitUtil.IsMolEmpty(Mol):
     115             MolName = RDKitUtil.GetMolName(Mol, MolCount)
     116             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
     117             continue
     118
     119         ValidMolCount += 1
     120
     121         if OptionsInfo["SkipConformerGeneration"]:
     122             Status = MinimizeMolecule(Mol, MolCount, Writer)
     123         else:
     124             Status = GenerateAndMinimizeConformers(Mol, MolCount, Writer)
     125
     126         if not Status:
     127             MinimizationFailedCount += 1
     128
     129     if Writer is not None:
     130         Writer.close()
     131
     132     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
     133     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
     134     MiscUtil.PrintInfo("Number of molecules failed during conformation generation or minimization: %d" % MinimizationFailedCount)
     135     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + MinimizationFailedCount))
     136
     137 def MinimizeMolecule(Mol, MolCount, Writer):
     138     "Minimize moleculer and write it out"
     139
     140     if  OptionsInfo["AddHydrogens"]:
     141         Mol = Chem.AddHs(Mol)
     142
     143     Status = 0
     144     try:
     145         if OptionsInfo["UseUFF"]:
     146             Status = AllChem.UFFOptimizeMolecule(Mol, maxIters = OptionsInfo["MaxIters"])
     147         elif OptionsInfo["UseMMFF"]:
     148             Status = AllChem.MMFFOptimizeMolecule(Mol,  maxIters = OptionsInfo["MaxIters"])
     149         else:
     150             MiscUtil.PrintError("Minimization couldn't be performed: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"])
     151     except RuntimeError as ErrMsg:
     152         MolName = RDKitUtil.GetMolName(Mol, MolCount)
     153         MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg))
     154         return False
     155
     156     if Status != 0:
     157         MolName = RDKitUtil.GetMolName(Mol, MolCount)
     158         MiscUtil.PrintWarning("Minimization failed to converge for molecule %s in %d steps. Try using higher value for \"--maxIters\" option...\n" % (MolName, OptionsInfo["MaxIters"]))
     159
     160     if  OptionsInfo["RemoveHydrogens"]:
     161         Mol = Chem.RemoveHs(Mol)
     162
     163     Writer.write(Mol)
     164
     165     return True
     166
     167 def GenerateAndMinimizeConformers(Mol, MolCount, Writer):
     168     "Generate and mininize conformers for a molecule and write out the lowest energy conformer."
     169
     170     if  OptionsInfo["AddHydrogens"]:
     171         Mol = Chem.AddHs(Mol)
     172
     173     ConfIDs = EmbedMolecule(Mol)
     174     if not len(ConfIDs):
     175         MolName = RDKitUtil.GetMolName(Mol, MolCount)
     176         MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s: Embedding failed...\n" % MolName)
     177         return False
     178
     179     CalcEnergyMap = {}
     180     for ConfID in ConfIDs:
     181         try:
     182             if OptionsInfo["UseUFF"]:
     183                 Status = AllChem.UFFOptimizeMolecule(Mol, confId = ConfID, maxIters = OptionsInfo["MaxIters"])
     184             elif OptionsInfo["UseMMFF"]:
     185                 Status = AllChem.MMFFOptimizeMolecule(Mol, confId = ConfID, maxIters = OptionsInfo["MaxIters"])
     186             else:
     187                 MiscUtil.PrintError("Minimization couldn't be performed: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"])
     188         except RuntimeError as ErrMsg:
     189             MolName = RDKitUtil.GetMolName(Mol, MolCount)
     190             MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg))
     191             return False
     192
     193         EnergyStatus, Energy = GetConformerEnergy(Mol, ConfID)
     194         if not EnergyStatus:
     195             MolName = RDKitUtil.GetMolName(Mol, MolCount)
     196             MiscUtil.PrintWarning("Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n" % (ConfID, MolName))
     197             return False
     198
     199         if Status != 0:
     200             MolName = RDKitUtil.GetMolName(Mol, MolCount)
     201             MiscUtil.PrintWarning("Minimization failed to converge for conformation number %d of molecule %s in %d steps. Try using higher value for \"--maxIters\" option...\n" % (ConfID, MolName, OptionsInfo["MaxIters"]))
     202
     203         CalcEnergyMap[ConfID] = Energy
     204
     205     SortedConfIDs = sorted(ConfIDs, key = lambda ConfID: CalcEnergyMap[ConfID])
     206     MinEnergyConfID = SortedConfIDs[0]
     207
     208     if  OptionsInfo["RemoveHydrogens"]:
     209         Mol = Chem.RemoveHs(Mol)
     210
     211     Writer.write(Mol, confId = MinEnergyConfID)
     212
     213     return True
     214
     215 def GetConformerEnergy(Mol, ConfID):
     216     "Calculate conformer energy"
     217
     218     Status = True
     219     Energy = 0.0
     220
     221     if OptionsInfo["UseUFF"]:
     222         UFFMoleculeForcefield = AllChem.UFFGetMoleculeForceField(Mol, confId = ConfID)
     223         if UFFMoleculeForcefield is None:
     224             Status = False
     225         else:
     226             Energy = UFFMoleculeForcefield.CalcEnergy()
     227     elif OptionsInfo["UseMMFF"]:
     228         MMFFMoleculeProperties = AllChem.MMFFGetMoleculeProperties(Mol)
     229         MMFFMoleculeForcefield = AllChem.MMFFGetMoleculeForceField(Mol, MMFFMoleculeProperties, confId = ConfID)
     230         if MMFFMoleculeForcefield is None:
     231             Status = False
     232         else:
     233             Energy = MMFFMoleculeForcefield.CalcEnergy()
     234     else:
     235         MiscUtil.PrintError("Couldn't retrieve conformer energy: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"])
     236
     237     return (Status, Energy)
     238
     239 def EmbedMolecule(Mol):
     240     "Embed conformations"
     241
     242     ConfIDs = []
     243
     244     MaxConfs = OptionsInfo["MaxConfs"]
     245     RandomSeed = OptionsInfo["RandomSeed"]
     246     EnforceChirality = OptionsInfo["EnforceChirality"]
     247     UseExpTorsionAnglePrefs = OptionsInfo["UseExpTorsionAnglePrefs"]
     248     UseBasicKnowledge = OptionsInfo["UseBasicKnowledge"]
     249
     250     ConfIDs = AllChem.EmbedMultipleConfs(Mol, numConfs = MaxConfs, randomSeed = RandomSeed, enforceChirality = EnforceChirality, useExpTorsionAnglePrefs = UseExpTorsionAnglePrefs, useBasicKnowledge = UseBasicKnowledge)
     251
     252     return ConfIDs
     253
     254 def ProcessOptions():
     255     """Process and validate command line arguments and options"""
     256
     257     MiscUtil.PrintInfo("Processing options...")
     258
     259     # Validate options...
     260     ValidateOptions()
     261
     262     OptionsInfo["Infile"] = Options["--infile"]
     263     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters("--infileParams", Options["--infileParams"], Options["--infile"])
     264
     265     OptionsInfo["Outfile"] = Options["--outfile"]
     266     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters("--outfileParams", Options["--outfileParams"])
     267
     268     OptionsInfo["Overwrite"] = Options["--overwrite"]
     269
     270     OptionsInfo["AddHydrogens"] = True
     271     if re.match("^no$", Options["--addHydrogens"], re.I):
     272         OptionsInfo["AddHydrogens"] = False
     273
     274     if re.match("^ETDG$", Options["--conformerGenerator"], re.I):
     275         ConformerGenerator = "ETDG"
     276         UseExpTorsionAnglePrefs = True
     277         UseBasicKnowledge = False
     278         SkipConformerGeneration = False
     279     elif re.match("^KDG$", Options["--conformerGenerator"], re.I):
     280         ConformerGenerator = "KDG"
     281         UseExpTorsionAnglePrefs = False
     282         UseBasicKnowledge = True
     283         SkipConformerGeneration = False
     284     elif re.match("^ETKDG$", Options["--conformerGenerator"], re.I):
     285         ConformerGenerator = "ETKDG"
     286         UseExpTorsionAnglePrefs = True
     287         UseBasicKnowledge = True
     288         SkipConformerGeneration = False
     289     elif re.match("^SDG$", Options["--conformerGenerator"], re.I):
     290         ConformerGenerator = "SDG"
     291         UseExpTorsionAnglePrefs = False
     292         UseBasicKnowledge = False
     293         SkipConformerGeneration = False
     294     else:
     295         ConformerGenerator = "None"
     296         UseExpTorsionAnglePrefs = False
     297         UseBasicKnowledge = False
     298         SkipConformerGeneration = True
     299
     300     OptionsInfo["SkipConformerGeneration"] = SkipConformerGeneration
     301     OptionsInfo["ConformerGenerator"] = ConformerGenerator
     302     OptionsInfo["UseExpTorsionAnglePrefs"] = UseExpTorsionAnglePrefs
     303     OptionsInfo["UseBasicKnowledge"] = UseBasicKnowledge
     304
     305     if re.match("^UFF$", Options["--forceField"], re.I):
     306         ForceField = "UFF"
     307         UseUFF = True
     308         UseMMFF = False
     309     elif re.match("^MMFF$", Options["--forceField"], re.I):
     310         ForceField = "MMFF"
     311         UseUFF = False
     312         UseMMFF = True
     313
     314     OptionsInfo["ForceField"] = ForceField
     315     OptionsInfo["UseMMFF"] = UseMMFF
     316     OptionsInfo["UseUFF"] = UseUFF
     317
     318     OptionsInfo["EnforceChirality"] = True
     319     if re.match("^no$", Options["--enforceChirality"], re.I):
     320         OptionsInfo["EnforceChirality"] = False
     321
     322     OptionsInfo["MaxIters"] = int(Options["--maxIters"])
     323     OptionsInfo["MaxConfs"] = int(Options["--maxConfs"])
     324
     325     RandomSeed = -1
     326     if not re.match("^auto$", Options["--randomSeed"], re.I):
     327         RandomSeed = int(Options["--randomSeed"])
     328     OptionsInfo["RandomSeed"] = RandomSeed
     329
     330     OptionsInfo["RemoveHydrogens"] = True
     331     if re.match("^no$", Options["--removeHydrogens"], re.I):
     332         OptionsInfo["RemoveHydrogens"] = False
     333
     334 def RetrieveOptions():
     335     """Retrieve command line arguments and options"""
     336
     337     # Get options...
     338     global Options
     339     Options = docopt(_docoptUsage_)
     340
     341     # Set current working directory to the specified directory...
     342     WorkingDir = Options["--workingdir"]
     343     if WorkingDir:
     344         os.chdir(WorkingDir)
     345
     346     # Handle examples option...
     347     if "--examples" in Options and Options["--examples"]:
     348         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
     349         sys.exit(0)
     350
     351 def ValidateOptions():
     352     """Validate option values"""
     353
     354     MiscUtil.ValidateOptionTextValue("-a, --addHydrogens", Options["--addHydrogens"], "yes no")
     355     MiscUtil.ValidateOptionTextValue("-c, --conformerGenerator", Options["--conformerGenerator"], "SDG ETDG KDG ETKDG None")
     356     MiscUtil.ValidateOptionTextValue("-f, --forceField", Options["--forceField"], "UFF MMFF")
     357
     358     MiscUtil.ValidateOptionTextValue("--enforceChirality ", Options["--enforceChirality"], "yes no")
     359
     360     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
     361     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv")
     362
     363     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd")
     364     MiscUtil.ValidateOptionsOutputFileOverwrite("-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"])
     365     MiscUtil.ValidateOptionsDistinctFileNames("-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"])
     366
     367     MiscUtil.ValidateOptionIntegerValue("--maxConfs", Options["--maxConfs"], {">": 0})
     368     MiscUtil.ValidateOptionIntegerValue("--maxIters", Options["--maxIters"], {">": 0})
     369
     370     if not re.match("^auto$", Options["--randomSeed"], re.I):
     371         MiscUtil.ValidateOptionIntegerValue("--randomSeed", Options["--randomSeed"], {})
     372         RandomSeed = int(Options["--randomSeed"])
     373
     374     MiscUtil.ValidateOptionTextValue("-r, --removeHydrogens", Options["--removeHydrogens"], "yes no")
     375
     376 # Setup a usage string for docopt...
     377 _docoptUsage_ = """
     378 RDKitPerformMinimization.py - Perform structure minimization
     379
     380 Usage:
     381     RDKitPerformMinimization.py [--addHydrogens <yes or no>] [--conformerGenerator <SDG, ETDG, KDG, ETKDG, None> ]
     382                                 [--forceField <UFF or MMFF>] [--enforceChirality <yes or no>] [--infileParams <Name,Value,...>]
     383                                 [--maxConfs <number>] [--maxIters <number>] [ --outfileParams <Name,Value,...> ]
     384                                 [--overwrite] [ --removeHydrogens <yes or no>] [--randomSeed <number>] [-w <dir>] -i <infile> -o <outfile>
     385     RDKitPerformMinimization.py -h | --help | -e | --examples
     386
     387 Description:
     388     Generate 3D structures for molecules using combination of distance geometry
     389     and forcefield minimization or minimize existing 3D structures using a specified
     390     forcefield.
     391
     392     The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi)
     393     .csv, .tcsv .txt)
     394
     395     The supported output file formats are: SD (.sdf, .sd)
     396
     397 Options:
     398     -a, --addHydrogens <yes or no>  [default: yes]
     399         Add hydrogens before minimization.
     400     -c, --conformerGenerator <SDG, ETDG, KDG, ETKDG, None>  [default: ETKDG]
     401         Conformation generation methodology for generating initial 3D coordinates.
     402         Possible values: Standard Distance Geometry, (SDG), Experimental Torsion-angle
     403         preference with Distance Geometry (ETDG), basic Knowledge-terms with Distance
     404         Geometry (KDG),  and Experimental Torsion-angle preference along with basic
     405         Knowledge-terms with Distance Geometry (ETKDG) [Ref 129] .
     406
     407         The conformation generation step may be skipped by specifying 'None' value to
     408         perform only forcefield minimization of molecules with 3D structures in input
     409         file.  This doesn't work for molecules in SMILES file or molecules in SD/MOL files
     410         containing 2D structures.
     411     -f, --forceField <UFF or MMFF>  [default: MMFF]
     412         Forcefield method to use for energy minimization. Possible values: Universal Force
     413         Field (UFF) [ Ref 81 ] or Merck Molecular Mechanics Force Field (MMFF) [ Ref 83-87 ] .
     414     --enforceChirality <yes or no>  [default: Yes]
     415         Enforce chirality for defined chiral centers.
     416     -e, --examples
     417         Print examples.
     418     -h, --help
     419         Print this help message.
     420     -i, --infile <infile>
     421         Input file name.
     422     --infileParams <Name,Value,...>  [default: auto]
     423         A comma delimited list of parameter name and value pairs for reading
     424         molecules from files. The supported parameter names for different file
     425         formats, along with their default values, are shown below:
     426
     427             SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
     428             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
     429                 smilesTitleLine,auto,sanitize,yes
     430
     431         Possible values for smilesDelimiter: space, comma or tab.
     432     --maxConfs <number>  [default: 250]
     433         Maximum number of conformations to generate for each molecule by conformation
     434         generation methodology for initial 3D coordinates. The conformations are minimized
     435         using the specified forcefield and the lowest energy conformation is written to the
     436         output file. This option is ignored during 'None' value of '-c --conformerGenerator'
     437         option.
     438     --maxIters <number>  [default: 500]
     439         Maximum number of iterations to perform for each molecule during forcefield
     440         minimization.
     441     -o, --outfile <outfile>
     442         Output file name.
     443     --outfileParams <Name,Value,...>  [default: auto]
     444         A comma delimited list of parameter name and value pairs for writing
     445         molecules to files. The supported parameter names for different file
     446         formats, along with their default values, are shown below:
     447
     448             SD: kekulize,no
     449
     450     --overwrite
     451         Overwrite existing files.
     452     -r, --removeHydrogens <yes or no>  [default: Yes]
     453         Remove hydrogens after minimization.
     454     --randomSeed <number>  [default: auto]
     455         Seed for the random number generator for reproducing 3D coordinates.
     456         Default is to use a random seed.
     457     -w, --workingdir <dir>
     458         Location of working directory which defaults to the current directory.
     459
     460 Examples:
     461     To generate up to 250 conformations using ETKDG methodology followed by MMFF
     462     forcefield minimization for a maximum of 500 iterations for molecules in a SMILES file
     463     and write out a SD file containing minimum energy structurse corresponding to each
     464     molecule, type
     465
     466         % RDKitPerformMinimization.py  -i Sample.smi -o SampleOut.sdf
     467
     468     To generate up to 150 conformations using ETKDG methodology followed by MMFF
     469     forcefield minimization for a maximum of 250 iterations along with a specified random
     470     seed  for molecules in a SMILES file and write out a SD file containing minimum energy
     471     structures corresponding to each molecule, type
     472
     473         % RDKitPerformMinimization.py  --maxConfs 150  --randomSeed 201780117
     474           --maxIters 250  -i Sample.smi -o SampleOut.sdf
     475
     476     To minimize structures in a 3D SD file using UFF forcefield for a maximum of 150
     477     iterations without generating any conformations and write out a SD file containing
     478     minimum energy structures corresponding to each molecule, type
     479
     480         % RDKitPerformMinimization.py  -c None -f UFF --maxIters 150
     481           -i Sample3D.sdf -o SampleOut.sdf
     482
     483     To generate up to 50 conformations using SDG methodology followed
     484     by UFF forcefield minimization for a maximum of 50 iterations for
     485     molecules in a CSV SMILES file, SMILES strings in column 1, name in
     486     column 2, and write out a SD file, type:
     487
     488         % RDKitPerformMinimization.py  --maxConfs 50  --maxIters 50 -c SDG
     489           -f UFF --infileParams "smilesDelimiter,comma,smilesTitleLine,yes,
     490           smilesColumn,1,smilesNameColumn,2"  -i SampleSMILES.csv
     491           -o SampleOut.sdf
     492
     493 Author:
     494     Manish Sud(msud@san.rr.com)
     495
     496 See also:
     497     RDKitCalculateRMSD.py, RDKitCalculateMolecularDescriptors.py, RDKitCompareMoleculeShapes.py,
     498     RDKitConvertFileFormat.py, RDKitGenerateConformers.py
     499
     500 Copyright:
     501     Copyright (C) 2018 Manish Sud. All rights reserved.
     502
     503     The functionality available in this script is implemented using RDKit, an
     504     open source toolkit for cheminformatics developed by Greg Landrum.
     505
     506     This file is part of MayaChemTools.
     507
     508     MayaChemTools is free software; you can redistribute it and/or modify it under
     509     the terms of the GNU Lesser General Public License as published by the Free
     510     Software Foundation; either version 3 of the License, or (at your option) any
     511     later version.
     512
     513 """
     514
     515 if __name__ == "__main__":
     516     main()