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()