1 #!/bin/env python 2 # 3 # File: RDKitGenerateMolecularFrameworks.py 4 # Author: Manish Sud <msud@san.rr.com> 5 # 6 # Copyright (C) 2026 Manish Sud. All rights reserved. 7 # 8 # The functionality available in this script is implemented using RDKit, an 9 # open source toolkit for cheminformatics developed by Greg Landrum. 10 # 11 # This file is part of MayaChemTools. 12 # 13 # MayaChemTools is free software; you can redistribute it and/or modify it under 14 # the terms of the GNU Lesser General Public License as published by the Free 15 # Software Foundation; either version 3 of the License, or (at your option) any 16 # later version. 17 # 18 # MayaChemTools is distributed in the hope that it will be useful, but without 19 # any warranty; without even the implied warranty of merchantability of fitness 20 # for a particular purpose. See the GNU Lesser General Public License for more 21 # details. 22 # 23 # You should have received a copy of the GNU Lesser General Public License 24 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or 25 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330, 26 # Boston, MA, 02111-1307, USA. 27 # 28 29 from __future__ import print_function 30 31 import os 32 import sys 33 import time 34 import re 35 36 # RDKit imports... 37 try: 38 from rdkit import rdBase 39 from rdkit import Chem 40 from rdkit.Chem import AllChem 41 from rdkit.Chem.Scaffolds import MurckoScaffold 42 except ImportError as ErrMsg: 43 sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg) 44 sys.stderr.write("Check/update your RDKit environment and try again.\n\n") 45 sys.exit(1) 46 47 # MayaChemTools imports... 48 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python")) 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 63 def main(): 64 """Start execution of the script.""" 65 66 MiscUtil.PrintInfo( 67 "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n" 68 % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime()) 69 ) 70 71 (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime() 72 73 # Retrieve command line arguments and options... 74 RetrieveOptions() 75 76 # Process and validate command line arguments and options... 77 ProcessOptions() 78 79 # Perform actions required by the script... 80 GenerateMolecularFrameworks() 81 82 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName) 83 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime)) 84 85 86 def GenerateMolecularFrameworks(): 87 """Generate Bemis Murcko molecular framworks.""" 88 89 Infile = OptionsInfo["Infile"] 90 Outfile = OptionsInfo["Outfile"] 91 92 UseChirality = OptionsInfo["UseChirality"] 93 94 RemoveDuplicateFrameworks = OptionsInfo["RemoveDuplicateFrameworks"] 95 UseGraphFrameworks = OptionsInfo["UseGraphFrameworks"] 96 97 SortFrameworks = OptionsInfo["SortFrameworks"] 98 if SortFrameworks: 99 FrameworkMolIDs = [] 100 FrameworkMolIDToMolMap = {} 101 FrameworkMolIDToAtomCountMap = {} 102 103 DuplicateFrameworkMolIDs = [] 104 DuplicateFrameworkMolIDToMolMap = {} 105 DuplicateFrameworkMolIDToAtomCountMap = {} 106 107 DuplicatesOutfile = "" 108 if RemoveDuplicateFrameworks: 109 DuplicatesOutfile = OptionsInfo["DuplicatesOutfile"] 110 111 # Setup a molecule reader... 112 MiscUtil.PrintInfo("\nProcessing file %s..." % Infile) 113 Mols = RDKitUtil.ReadMolecules(Infile, **OptionsInfo["InfileParams"]) 114 115 # Set up a molecular framework writer... 116 Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"]) 117 if Writer is None: 118 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile) 119 120 # Set up a duplicate molecular framework writer... 121 if RemoveDuplicateFrameworks: 122 DuplicatesWriter = RDKitUtil.MoleculesWriter(DuplicatesOutfile, **OptionsInfo["OutfileParams"]) 123 if Writer is None: 124 MiscUtil.PrintError("Failed to setup a writer for duplicates output fie %s " % DuplicatesOutfile) 125 126 if RemoveDuplicateFrameworks: 127 MiscUtil.PrintInfo("Generating files: %s and %s..." % (Outfile, DuplicatesOutfile)) 128 else: 129 MiscUtil.PrintInfo("Generating file %s..." % Outfile) 130 131 # Process molecules... 132 MolCount = 0 133 ValidMolCount = 0 134 135 FrameworksCount = 0 136 UniqueFrameworksCount = 0 137 DuplicateFrameworksCount = 0 138 139 CanonicalSMILESMap = {} 140 141 Compute2DCoords = OptionsInfo["OutfileParams"]["Compute2DCoords"] 142 143 for Mol in Mols: 144 MolCount += 1 145 146 if Mol is None: 147 continue 148 149 if RDKitUtil.IsMolEmpty(Mol): 150 MolName = RDKitUtil.GetMolName(Mol, MolCount) 151 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 152 continue 153 154 ValidMolCount += 1 155 156 if UseGraphFrameworks: 157 FrameworksMol = MurckoScaffold.MakeScaffoldGeneric(Mol) 158 else: 159 FrameworksMol = MurckoScaffold.GetScaffoldForMol(Mol) 160 161 if Compute2DCoords: 162 AllChem.Compute2DCoords(FrameworksMol) 163 164 if SortFrameworks: 165 HeavyAtomCount = FrameworksMol.GetNumHeavyAtoms() 166 167 FrameworksCount += 1 168 169 if RemoveDuplicateFrameworks: 170 CanonicalSMILES = Chem.MolToSmiles(FrameworksMol, isomericSmiles=UseChirality, canonical=True) 171 if CanonicalSMILES in CanonicalSMILESMap: 172 DuplicateFrameworksCount += 1 173 if SortFrameworks: 174 # Track duplicate frameworks... 175 DuplicateFrameworkMolIDs.append(DuplicateFrameworksCount) 176 DuplicateFrameworkMolIDToMolMap[DuplicateFrameworksCount] = FrameworksMol 177 DuplicateFrameworkMolIDToAtomCountMap[DuplicateFrameworksCount] = HeavyAtomCount 178 else: 179 # Write it out... 180 DuplicatesWriter.write(FrameworksMol) 181 else: 182 UniqueFrameworksCount += 1 183 CanonicalSMILESMap[CanonicalSMILES] = CanonicalSMILES 184 if SortFrameworks: 185 # Track unique frameworks... 186 FrameworkMolIDs.append(UniqueFrameworksCount) 187 FrameworkMolIDToMolMap[UniqueFrameworksCount] = FrameworksMol 188 FrameworkMolIDToAtomCountMap[UniqueFrameworksCount] = HeavyAtomCount 189 else: 190 # Write it out... 191 Writer.write(FrameworksMol) 192 elif SortFrameworks: 193 # Track for sorting... 194 FrameworkMolIDs.append(FrameworksCount) 195 FrameworkMolIDToMolMap[FrameworksCount] = FrameworksMol 196 FrameworkMolIDToAtomCountMap[FrameworksCount] = HeavyAtomCount 197 else: 198 # Write it out... 199 Writer.write(FrameworksMol) 200 201 if SortFrameworks: 202 ReverseOrder = OptionsInfo["DescendingSortOrder"] 203 SortAndWriteFrameworks( 204 Writer, FrameworkMolIDs, FrameworkMolIDToMolMap, FrameworkMolIDToAtomCountMap, ReverseOrder 205 ) 206 if RemoveDuplicateFrameworks: 207 SortAndWriteFrameworks( 208 DuplicatesWriter, 209 DuplicateFrameworkMolIDs, 210 DuplicateFrameworkMolIDToMolMap, 211 DuplicateFrameworkMolIDToAtomCountMap, 212 ReverseOrder, 213 ) 214 215 Writer.close() 216 if RemoveDuplicateFrameworks: 217 DuplicatesWriter.close() 218 219 MiscUtil.PrintInfo("\nTotal number of molecular frameworks: %d" % FrameworksCount) 220 if RemoveDuplicateFrameworks: 221 MiscUtil.PrintInfo("Number of unique molecular frameworks: %d" % UniqueFrameworksCount) 222 MiscUtil.PrintInfo("Number of duplicate molecular frameworks: %d" % DuplicateFrameworksCount) 223 224 MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount) 225 MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount) 226 MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount)) 227 228 229 def SortAndWriteFrameworks(MolWriter, MolIDs, MolIDToMolMap, MolIDToAtomCountMap, ReverseOrder): 230 """Sort frameworks and write them out.""" 231 SortedMolIDs = sorted(MolIDs, key=lambda MolID: MolIDToAtomCountMap[MolID], reverse=ReverseOrder) 232 for MolID in SortedMolIDs: 233 FrameworksMol = MolIDToMolMap[MolID] 234 MolWriter.write(FrameworksMol) 235 236 237 def ProcessOptions(): 238 """Process and validate command line arguments and options.""" 239 240 MiscUtil.PrintInfo("Processing options...") 241 242 # Validate options... 243 ValidateOptions() 244 245 OptionsInfo["Infile"] = Options["--infile"] 246 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters( 247 "--infileParams", Options["--infileParams"], Options["--infile"] 248 ) 249 250 OptionsInfo["Outfile"] = Options["--outfile"] 251 OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters( 252 "--outfileParams", Options["--outfileParams"], Options["--infile"], Options["--outfile"] 253 ) 254 255 OptionsInfo["Overwrite"] = Options["--overwrite"] 256 257 OptionsInfo["Mode"] = Options["--mode"] 258 OptionsInfo["UseGraphFrameworks"] = False 259 if re.match("^GraphFrameworks$", OptionsInfo["Mode"], re.I): 260 OptionsInfo["UseGraphFrameworks"] = True 261 262 OptionsInfo["RemoveDuplicates"] = Options["--removeDuplicates"] 263 OptionsInfo["RemoveDuplicateFrameworks"] = False 264 if re.match("^Yes$", OptionsInfo["RemoveDuplicates"], re.I): 265 OptionsInfo["RemoveDuplicateFrameworks"] = True 266 267 # Setup outfile for writing out duplicates... 268 OptionsInfo["DuplicatesOutfile"] = "" 269 if OptionsInfo["RemoveDuplicateFrameworks"]: 270 FileDir, FileName, FileExt = MiscUtil.ParseFileName(OptionsInfo["Outfile"]) 271 OptionsInfo["DuplicatesOutfile"] = "%sDuplicates.%s" % (FileName, FileExt) 272 273 OptionsInfo["Sort"] = Options["--sort"] 274 OptionsInfo["SortFrameworks"] = False 275 if re.match("^Yes$", OptionsInfo["Sort"], re.I): 276 OptionsInfo["SortFrameworks"] = True 277 278 OptionsInfo["SortOrder"] = Options["--sortOrder"] 279 OptionsInfo["DescendingSortOrder"] = False 280 if re.match("^Descending$", OptionsInfo["SortOrder"], re.I): 281 OptionsInfo["DescendingSortOrder"] = True 282 283 OptionsInfo["UseChirality"] = False 284 if re.match("^yes$", Options["--useChirality"], re.I): 285 OptionsInfo["UseChirality"] = True 286 287 288 def RetrieveOptions(): 289 """Retrieve command line arguments and options.""" 290 291 # Get options... 292 global Options 293 Options = docopt(_docoptUsage_) 294 295 # Set current working directory to the specified directory... 296 WorkingDir = Options["--workingdir"] 297 if WorkingDir: 298 os.chdir(WorkingDir) 299 300 # Handle examples option... 301 if "--examples" in Options and Options["--examples"]: 302 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_)) 303 sys.exit(0) 304 305 306 def ValidateOptions(): 307 """Validate option values.""" 308 309 MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"]) 310 MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd smi txt csv tsv") 311 312 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd smi") 313 MiscUtil.ValidateOptionsOutputFileOverwrite( 314 "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"] 315 ) 316 MiscUtil.ValidateOptionsDistinctFileNames( 317 "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"] 318 ) 319 320 MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "GraphFrameworks AtomicFrameworks") 321 322 MiscUtil.ValidateOptionTextValue("-r, --removeDuplicates", Options["--removeDuplicates"], "yes no") 323 MiscUtil.ValidateOptionTextValue("-s, --sort", Options["--sort"], "yes no") 324 MiscUtil.ValidateOptionTextValue("--sortOrder", Options["--sortOrder"], "ascending descending") 325 326 MiscUtil.ValidateOptionTextValue("--useChirality", Options["--useChirality"], "yes no") 327 328 329 # Setup a usage string for docopt... 330 _docoptUsage_ = """ 331 RDKitGenerateMolecularFrameworks.py - Generate Bemis Murcko molecular frameworks 332 333 Usage: 334 RDKitGenerateMolecularFrameworks.py [--infileParams <Name,Value,...>] 335 [--mode <GraphFrameworks or AtomicFrameworks> ] 336 [ --outfileParams <Name,Value,...> ] [--overwrite] [--removeDuplicates <yes or no>] 337 [--sort <yes or no>] [--sortOrder <ascending or descending>] 338 [--useChirality <yes or no>] [-w <dir>] -i <infile> -o <outfile> 339 RDKitGenerateMolecularFrameworks.py -h | --help | -e | --examples 340 341 Description: 342 Generate Bemis Murcko [ Ref 133 ] molecular frameworks for molecules. Two types of molecular 343 frameworks can be generated: Graph or atomic frameworks. The graph molecular framework 344 is a generic framework. The atom type, hybridization, and bond order is ignore during its 345 generation. All atoms are set to carbon atoms and all bonds are single bonds. The atom type, 346 hybridization, and bond order is preserved during generation of atomic molecular frameworks. 347 348 The supported input file formats are: SD (.sdf, .sd), SMILES (.smi, .csv, .tsv, .txt) 349 350 The supported output file formats are: SD (.sdf, .sd), SMILES (.smi) 351 352 Options: 353 -e, --examples 354 Print examples. 355 -h, --help 356 Print this help message. 357 -i, --infile <infile> 358 Input file name. 359 --infileParams <Name,Value,...> [default: auto] 360 A comma delimited list of parameter name and value pairs for reading 361 molecules from files. The supported parameter names for different file 362 formats, along with their default values, are shown below: 363 364 SD: removeHydrogens,yes,sanitize,yes,strictParsing,yes 365 SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space, 366 smilesTitleLine,auto,sanitize,yes 367 368 Possible values for smilesDelimiter: space, comma or tab. 369 -m, --mode <GraphFrameworks or AtomicFrameworks> [default: GraphFrameworks] 370 Type of molecular frameworks to generate for molecules. Possible values: GraphFrameworks 371 or AtomicFrameworks. The graph molecular framework is a generic framework. The atom type, 372 hybridization, and bond order is ignore during its generation. All atoms are set to carbon atoms 373 and all bonds are single bonds. The atom type, hybridization, and bond order is preserved 374 during the generation of atomic molecular frameworks. 375 -o, --outfile <outfile> 376 Output file name. 377 --outfileParams <Name,Value,...> [default: auto] 378 A comma delimited list of parameter name and value pairs for writing 379 molecules to files. The supported parameter names for different file 380 formats, along with their default values, are shown below: 381 382 SD: compute2DCoords,auto,forceV3000,no 383 SMILES: smilesDelimiter,space,smilesTitleLine,yes 384 385 Default value for compute2DCoords: yes for SMILES input file; no for all other 386 file types. 387 --overwrite 388 Overwrite existing files. 389 -r, --removeDuplicates <yes or no> [default: no] 390 Remove duplicate molecular frameworks. Possible values: yes or no. The duplicate 391 molecular franworks are identified using canonical SMILES. The removed frameworks 392 are written to a separate output file. 393 -s, --sort <yes or no> [default: no] 394 Sort molecular frameworks by heavy atom count. Possible values: yes or no. 395 --sortOrder <ascending or descending> [default: ascending] 396 Sorting order for molecular frameworks. Possible values: ascending or descending. 397 -u, --useChirality <yes or no> [default: yes] 398 Use stereochemistry for generation of canonical SMILES strings to identify 399 duplicate molecular frameworks. 400 -w, --workingdir <dir> 401 Location of working directory which defaults to the current directory. 402 403 Examples: 404 To generate graph molecular framworks for molecules and write out a SMILES file, 405 type: 406 407 % RDKitGenerateMolecularFrameworks.py -i Sample.smi -o SampleOut.smi 408 409 To generate graph molecular framworks, remove duplicate frameworks for molecules 410 and write out SD files for unique and duplicate frameworks, type: 411 412 % RDKitGenerateMolecularFrameworks.py -m GraphFrameworks -r yes 413 -i Sample.sdf -o SampleOut.sdf 414 415 To generate atomic molecular framworks, remove duplicate frameworks, sort 416 framworks by heavy atom count in ascending order, write out SMILES files for 417 unique and duplicate frameworks, type: 418 419 % RDKitGenerateMolecularFrameworks.py -m AtomicFrameworks -r yes 420 -s yes -i Sample.smi -o SampleOut.smi 421 422 To generate graph molecular framworks for molecules in a CSV SMILES file, 423 SMILES strings in column 1, name in olumn 2, emove duplicate frameworks, 424 sort framworks by heavy atom count in decending order and write out a SD 425 file, type: 426 427 % RDKitGenerateMolecularFrameworks.py -m AtomicFrameworks 428 --removeDuplicates yes -s yes --sortOrder descending --infileParams 429 "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1, 430 smilesNameColumn,2" --outfileParams "compute2DCoords,yes" 431 -i SampleSMILES.csv -o SampleOut.sdf 432 433 Author: 434 Manish Sud(msud@san.rr.com) 435 436 See also: 437 RDKitConvertFileFormat.py, RDKitDrawMolecules.py, RDKitSearchFunctionalGroups.py, 438 RDKitSearchSMARTS.py 439 440 Copyright: 441 Copyright (C) 2026 Manish Sud. All rights reserved. 442 443 The functionality available in this script is implemented using RDKit, an 444 open source toolkit for cheminformatics developed by Greg Landrum. 445 446 This file is part of MayaChemTools. 447 448 MayaChemTools is free software; you can redistribute it and/or modify it under 449 the terms of the GNU Lesser General Public License as published by the Free 450 Software Foundation; either version 3 of the License, or (at your option) any 451 later version. 452 453 """ 454 455 if __name__ == "__main__": 456 main()