MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: RDKitEnumerateStereoisomers.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.Chem import AllChem
  40     from rdkit.Chem.EnumerateStereoisomers import EnumerateStereoisomers, StereoEnumerationOptions
  41 except ImportError as ErrMsg:
  42     sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
  43     sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
  44     sys.exit(1)
  45 
  46 # MayaChemTools imports...
  47 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  48 try:
  49     from docopt import docopt
  50     import MiscUtil
  51     import RDKitUtil
  52 except ImportError as ErrMsg:
  53     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  54     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  55     sys.exit(1)
  56 
  57 ScriptName = os.path.basename(sys.argv[0])
  58 Options = {}
  59 OptionsInfo = {}
  60 
  61 
  62 def main():
  63     """Start execution of the script."""
  64 
  65     MiscUtil.PrintInfo(
  66         "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
  67         % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
  68     )
  69 
  70     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  71 
  72     # Retrieve command line arguments and options...
  73     RetrieveOptions()
  74 
  75     # Process and validate command line arguments and options...
  76     ProcessOptions()
  77 
  78     # Perform actions required by the script...
  79     PerformEnumeration()
  80 
  81     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  82     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  83 
  84 
  85 def PerformEnumeration():
  86     """Enumerate stereoisomers."""
  87 
  88     Infile = OptionsInfo["Infile"]
  89     Outfile = OptionsInfo["Outfile"]
  90 
  91     # Setup a molecule reader...
  92     MiscUtil.PrintInfo("\nProcessing file %s..." % Infile)
  93     Mols = RDKitUtil.ReadMolecules(Infile, **OptionsInfo["InfileParams"])
  94 
  95     # Set up a molecule writer...
  96     Writer = None
  97     Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"])
  98     if Writer is None:
  99         MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile)
 100     MiscUtil.PrintInfo("Generating file %s...\n" % Outfile)
 101 
 102     # Setup stereo enumeration options...
 103     StereoOptions = StereoEnumerationOptions(
 104         tryEmbedding=OptionsInfo["DiscardNonPhysical"],
 105         onlyUnassigned=OptionsInfo["UnassignedOnly"],
 106         maxIsomers=OptionsInfo["MaxIsomers"],
 107     )
 108 
 109     # Process molecules...
 110     MolCount = 0
 111     ValidMolCount = 0
 112 
 113     Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"]
 114 
 115     for Mol in Mols:
 116         MolCount += 1
 117 
 118         if Mol is None:
 119             continue
 120 
 121         if RDKitUtil.IsMolEmpty(Mol):
 122             MolName = RDKitUtil.GetMolName(Mol, MolCount)
 123             MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
 124             continue
 125 
 126         ValidMolCount += 1
 127 
 128         MolName = RDKitUtil.GetMolName(Mol, MolCount)
 129 
 130         # Generate and process stereoisomers...
 131         StereoisomersMols = EnumerateStereoisomers(Mol, options=StereoOptions)
 132         IsomerCount = 0
 133         for IsomerMol in StereoisomersMols:
 134             IsomerCount += 1
 135 
 136             # Set isomer mol name...
 137             IsomerMolName = "%s_Isomer%d" % (MolName, IsomerCount)
 138             IsomerMol.SetProp("_Name", IsomerMolName)
 139 
 140             if Compute2DCoords:
 141                 AllChem.Compute2DCoords(IsomerMol)
 142 
 143             Writer.write(IsomerMol)
 144 
 145         MiscUtil.PrintInfo("Number of stereoisomers written for %s: %d" % (MolName, IsomerCount))
 146 
 147     if Writer is not None:
 148         Writer.close()
 149 
 150     MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
 151     MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
 152     MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount))
 153 
 154 
 155 def ProcessOptions():
 156     """Process and validate command line arguments and options."""
 157 
 158     MiscUtil.PrintInfo("Processing options...")
 159 
 160     # Validate options...
 161     ValidateOptions()
 162 
 163     OptionsInfo["DiscardNonPhysical"] = True
 164     if re.match("^no$", Options["--discardNonPhysical"], re.I):
 165         OptionsInfo["DiscardNonPhysical"] = False
 166 
 167     OptionsInfo["Mode"] = Options["--mode"]
 168     UnassignedOnly = True
 169     if re.match("^All$", Options["--mode"], re.I):
 170         UnassignedOnly = False
 171     OptionsInfo["UnassignedOnly"] = UnassignedOnly
 172 
 173     OptionsInfo["Infile"] = Options["--infile"]
 174     OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
 175         "--infileParams", Options["--infileParams"], Options["--infile"]
 176     )
 177 
 178     OptionsInfo["Outfile"] = Options["--outfile"]
 179     OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
 180         "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"]
 181     )
 182 
 183     OptionsInfo["Overwrite"] = Options["--overwrite"]
 184 
 185     OptionsInfo["MaxIsomers"] = int(Options["--maxIsomers"])
 186 
 187 
 188 def RetrieveOptions():
 189     """Retrieve command line arguments and options."""
 190 
 191     # Get options...
 192     global Options
 193     Options = docopt(_docoptUsage_)
 194 
 195     # Set current working directory to the specified directory...
 196     WorkingDir = Options["--workingdir"]
 197     if WorkingDir:
 198         os.chdir(WorkingDir)
 199 
 200     # Handle examples option...
 201     if "--examples" in Options and Options["--examples"]:
 202         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 203         sys.exit(0)
 204 
 205 
 206 def ValidateOptions():
 207     """Validate option values."""
 208 
 209     MiscUtil.ValidateOptionTextValue("-d, --discardNonPhysical", Options["--discardNonPhysical"], "yes no")
 210     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "UnassignedOnly All")
 211 
 212     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 213     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv")
 214 
 215     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi")
 216     MiscUtil.ValidateOptionsOutputFileOverwrite(
 217         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 218     )
 219     MiscUtil.ValidateOptionsDistinctFileNames(
 220         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 221     )
 222 
 223     MiscUtil.ValidateOptionIntegerValue("--maxIsomers", Options["--maxIsomers"], {">=": 0})
 224 
 225 
 226 # Setup a usage string for docopt...
 227 _docoptUsage_ = """
 228 RDKitEnumerateStereoisomers.py - Enumerate stereoisomers of molecules
 229 
 230 Usage:
 231     RDKitEnumerateStereoisomers.py [--discardNonPhysical <yes or no>]
 232                                 [--infileParams <Name,Value,...>] [--mode <UnassignedOnly or All>]
 233                                 [--maxIsomers <number>] [--outfileParams <Name,Value,...>] 
 234                                 [--overwrite] [-w <dir>] -i <infile> -o <outfile> 
 235     RDKitEnumerateStereoisomers.py -h | --help | -e | --examples
 236 
 237 Description:
 238     Perform a combinatorial enumeration of stereoisomers for molecules around all
 239     or unassigned chiral atoms and bonds.
 240 
 241     The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
 242     .csv, .tsv, .txt)
 243 
 244     The supported output file format are: SD (.sdf, .sd), SMILES (.smi)
 245 
 246 Options:
 247     -d, --discardNonPhysical <yes or no>  [default: yes]
 248         Discard stereoisomers with non-physical structures. Possible values: yes or no.
 249         The non-physical nature of a stereoisomer is determined by embedding the
 250         structure to generate a conformation for the stereoisomer using standard
 251         distance geometry methodology.
 252         
 253         A word to the wise from RDKit documentation: this is computationally expensive
 254         and uses a heuristic that could result in loss of stereoisomers.
 255     -e, --examples
 256         Print examples.
 257     -m, --mode <UnassignedOnly or All>  [default: UnassignedOnly]
 258         Enumerate unassigned or all chiral centers. The chiral atoms and bonds with
 259         defined stereochemistry are preserved.
 260     --maxIsomers <number>  [default: 50]
 261         Maximum number of stereoisomers to generate for each molecule. A  value of zero
 262         indicates generation of all possible steroisomers.
 263     -h, --help
 264         Print this help message.
 265     -i, --infile <infile>
 266         Input file name.
 267     --infileParams <Name,Value,...>  [default: auto]
 268         A comma delimited list of parameter name and value pairs for reading
 269         molecules from files. The supported parameter names for different file
 270         formats, along with their default values, are shown below:
 271             
 272             SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
 273             SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
 274                 smilesTitleLine,auto,sanitize,yes
 275             
 276         Possible values for smilesDelimiter: space, comma or tab.
 277     -o, --outfile <outfile>
 278         Output file name.
 279     --outfileParams <Name,Value,...>  [default: auto]
 280         A comma delimited list of parameter name and value pairs for writing
 281         molecules to files. The supported parameter names for different file
 282         formats, along with their default values, are shown below:
 283             
 284             SD: compute2DCoords,auto,kekulize,yes,forceV3000,no
 285             SMILES: smilesKekulize,no,smilesDelimiter,space, smilesIsomeric,yes,
 286                 smilesTitleLine,yes
 287             
 288         Default value for compute2DCoords: yes for SMILES input file; no for all other
 289         file types.
 290     --overwrite
 291         Overwrite existing files.
 292     -w, --workingdir <dir>
 293         Location of working directory which defaults to the current directory.
 294 
 295 Examples:
 296     To enumerate only unassigned atom and bond chiral centers along with discarding
 297     of non-physical structures, keeping a maximum of 50 stereoisomers for each molecule,
 298     and write out a SMILES file, type:
 299 
 300         % RDKitEnumerateStereoisomers.py  -i Sample.smi -o SampleOut.smi
 301 
 302     To enumerate only unassigned atom and bond chiral centers along with discarding
 303     any non-physical structures, keeping a maximum of 250 stereoisomers for a molecule,
 304     and write out a SD file, type:
 305 
 306         % RDKitEnumerateStereoisomers.py  --maxIsomers 0 -i Sample.smi
 307            --maxIsomers 250 -o SampleOut.sdf
 308 
 309     To enumerate all possible assigned and unassigned atom and bond chiral centers,
 310     without discarding any non-physical structures, keeping a maximum of 500 
 311     stereoisomers for a molecule, and write out a SD file, type:
 312 
 313         % RDKitEnumerateStereoisomers.py  -d no -m all --maxIsomers 500
 314           -i Sample.smi -o SampleOut.sdf
 315 
 316     To enumerate only unassigned atom and bond chiral centers along with discarding
 317     of non-physical structures, keeping a maximum of 50 stereoisomers for each molecule
 318     in a CSV SMILES file, SMILES strings in column 1, name in column 2, and write out a
 319     SD file without kekulization, type:
 320 
 321         % RDKitEnumerateStereoisomers.py  --infileParams 
 322           "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
 323           smilesNameColumn,2" --outfileParams "compute2DCoords,yes,
 324           kekulize,no" -i SampleSMILES.csv -o SampleOut.sdf
 325 
 326 Author:
 327     Manish Sud(msud@san.rr.com)
 328 
 329 See also:
 330     RDKitConvertFileFormat.py, RDKitEnumerateCompoundLibrary.py, RDKitGenerateConformers.py,
 331     RDKitGenerateMolecularFrameworks.py
 332 
 333 Copyright:
 334     Copyright (C) 2026 Manish Sud. All rights reserved.
 335 
 336     The functionality available in this script is implemented using RDKit, an
 337     open source toolkit for cheminformatics developed by Greg Landrum.
 338 
 339     This file is part of MayaChemTools.
 340 
 341     MayaChemTools is free software; you can redistribute it and/or modify it under
 342     the terms of the GNU Lesser General Public License as published by the Free
 343     Software Foundation; either version 3 of the License, or (at your option) any
 344     later version.
 345 
 346 """
 347 
 348 if __name__ == "__main__":
 349     main()