1 #!/bin/env python 2 # 3 # File: RDKitCalculateEnergy.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 37 # RDKit imports... 38 try: 39 from rdkit import rdBase 40 from rdkit import Chem 41 from rdkit.Chem import AllChem 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 CalculateEnergy() 81 82 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName) 83 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime)) 84 85 86 def CalculateEnergy(): 87 """Calculate single point energy.""" 88 89 # Setup a molecule reader... 90 MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"]) 91 Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"]) 92 93 # Set up a molecule writer... 94 Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"]) 95 if Writer is None: 96 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"]) 97 MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"]) 98 99 MolCount, ValidMolCount, EnergyFailedCount = ProcessMolecules(Mols, Writer) 100 101 if Writer is not None: 102 Writer.close() 103 104 MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount) 105 MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount) 106 MiscUtil.PrintInfo("Number of molecules failed during energy calculation: %d" % EnergyFailedCount) 107 MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + EnergyFailedCount)) 108 109 110 def ProcessMolecules(Mols, Writer): 111 """Process and calculate energy of molecules.""" 112 113 if OptionsInfo["MPMode"]: 114 return ProcessMoleculesUsingMultipleProcesses(Mols, Writer) 115 else: 116 return ProcessMoleculesUsingSingleProcess(Mols, Writer) 117 118 119 def ProcessMoleculesUsingSingleProcess(Mols, Writer): 120 """Process and calculate energy of molecules using a single process.""" 121 122 MiscUtil.PrintInfo("\nCalculating energy...") 123 124 (MolCount, ValidMolCount, EnergyFailedCount) = [0] * 3 125 for Mol in Mols: 126 MolCount += 1 127 128 if Mol is None: 129 continue 130 131 if RDKitUtil.IsMolEmpty(Mol): 132 if not OptionsInfo["QuietMode"]: 133 MolName = RDKitUtil.GetMolName(Mol, MolCount) 134 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 135 continue 136 137 ValidMolCount += 1 138 139 CalcStatus, Energy = CalculateMoleculeEnergy(Mol, MolCount) 140 if CalcStatus: 141 Energy = "%.2f" % Energy 142 else: 143 if not OptionsInfo["QuietMode"]: 144 MolName = RDKitUtil.GetMolName(Mol, MolCount) 145 MiscUtil.PrintWarning("Failed to calculate energy for molecule %s" % MolName) 146 147 EnergyFailedCount += 1 148 continue 149 150 WriteMolecule(Writer, Mol, Energy) 151 152 return (MolCount, ValidMolCount, EnergyFailedCount) 153 154 155 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer): 156 """Process and calculate energy of molecules using process.""" 157 158 MiscUtil.PrintInfo("\nCalculating energy using multiprocessing...") 159 160 MPParams = OptionsInfo["MPParams"] 161 162 # Setup data for initializing a worker process... 163 InitializeWorkerProcessArgs = ( 164 MiscUtil.ObjectToBase64EncodedString(Options), 165 MiscUtil.ObjectToBase64EncodedString(OptionsInfo), 166 ) 167 168 # Setup a encoded mols data iterable for a worker process... 169 WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols) 170 171 # Setup process pool along with data initialization for each process... 172 MiscUtil.PrintInfo( 173 "\nConfiguring multiprocessing using %s method..." 174 % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()") 175 ) 176 MiscUtil.PrintInfo( 177 "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n" 178 % ( 179 MPParams["NumProcesses"], 180 MPParams["InputDataMode"], 181 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]), 182 ) 183 ) 184 185 ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs) 186 187 # Start processing... 188 if re.match("^Lazy$", MPParams["InputDataMode"], re.I): 189 Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 190 elif re.match("^InMemory$", MPParams["InputDataMode"], re.I): 191 Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 192 else: 193 MiscUtil.PrintError( 194 'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"]) 195 ) 196 197 (MolCount, ValidMolCount, EnergyFailedCount) = [0] * 3 198 for Result in Results: 199 MolCount += 1 200 MolIndex, EncodedMol, CalcStatus, Energy = Result 201 202 if EncodedMol is None: 203 continue 204 ValidMolCount += 1 205 206 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 207 if CalcStatus: 208 Energy = "%.2f" % Energy 209 else: 210 if not OptionsInfo["QuietMode"]: 211 MolName = RDKitUtil.GetMolName(Mol, MolCount) 212 MiscUtil.PrintWarning("Failed to calculate energy for molecule %s" % MolName) 213 214 EnergyFailedCount += 1 215 continue 216 217 WriteMolecule(Writer, Mol, Energy) 218 219 return (MolCount, ValidMolCount, EnergyFailedCount) 220 221 222 def InitializeWorkerProcess(*EncodedArgs): 223 """Initialize data for a worker process.""" 224 225 global Options, OptionsInfo 226 227 MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid()) 228 229 # Decode Options and OptionInfo... 230 Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0]) 231 OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1]) 232 233 234 def WorkerProcess(EncodedMolInfo): 235 """Process data for a worker process.""" 236 237 MolIndex, EncodedMol = EncodedMolInfo 238 239 CalcStatus = False 240 Energy = None 241 242 if EncodedMol is None: 243 return [MolIndex, None, CalcStatus, Energy] 244 245 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 246 if RDKitUtil.IsMolEmpty(Mol): 247 if not OptionsInfo["QuietMode"]: 248 MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1)) 249 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 250 return [MolIndex, None, CalcStatus, Energy] 251 252 CalcStatus, Energy = CalculateMoleculeEnergy(Mol, (MolIndex + 1)) 253 254 return [ 255 MolIndex, 256 RDKitUtil.MolToBase64EncodedMolString( 257 Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps 258 ), 259 CalcStatus, 260 Energy, 261 ] 262 263 264 def CalculateMoleculeEnergy(Mol, MolNum=None): 265 "Calculate energy." 266 267 Status = True 268 Energy = None 269 ConfID = -1 270 271 if OptionsInfo["UseUFF"]: 272 UFFMoleculeForcefield = AllChem.UFFGetMoleculeForceField(Mol, confId=ConfID) 273 if UFFMoleculeForcefield is None: 274 Status = False 275 else: 276 Energy = UFFMoleculeForcefield.CalcEnergy() 277 elif OptionsInfo["UseMMFF"]: 278 MMFFMoleculeProperties = AllChem.MMFFGetMoleculeProperties(Mol, mmffVariant=OptionsInfo["MMFFVariant"]) 279 MMFFMoleculeForcefield = AllChem.MMFFGetMoleculeForceField(Mol, MMFFMoleculeProperties, confId=ConfID) 280 if MMFFMoleculeForcefield is None: 281 Status = False 282 else: 283 Energy = MMFFMoleculeForcefield.CalcEnergy() 284 else: 285 MiscUtil.PrintError( 286 "Couldn't calculate nergy: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"] 287 ) 288 289 return (Status, Energy) 290 291 292 def WriteMolecule(Writer, Mol, Energy): 293 """Write molecule.""" 294 295 Mol.SetProp(OptionsInfo["EnergyLabel"], Energy) 296 Writer.write(Mol) 297 298 299 def ProcessOptions(): 300 """Process and validate command line arguments and options.""" 301 302 MiscUtil.PrintInfo("Processing options...") 303 304 # Validate options... 305 ValidateOptions() 306 307 OptionsInfo["Infile"] = Options["--infile"] 308 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters( 309 "--infileParams", Options["--infileParams"], Options["--infile"] 310 ) 311 312 OptionsInfo["Outfile"] = Options["--outfile"] 313 OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters( 314 "--outfileParams", Options["--outfileParams"] 315 ) 316 317 OptionsInfo["Overwrite"] = Options["--overwrite"] 318 319 if re.match("^UFF$", Options["--forceField"], re.I): 320 ForceField = "UFF" 321 UseUFF = True 322 UseMMFF = False 323 elif re.match("^MMFF$", Options["--forceField"], re.I): 324 ForceField = "MMFF" 325 UseUFF = False 326 UseMMFF = True 327 else: 328 MiscUtil.PrintError( 329 'The value, %s, specified for "--forceField" is not supported.' % (Options["--forceField"],) 330 ) 331 332 MMFFVariant = "MMFF94" if re.match("^MMFF94$", Options["--forceFieldMMFFVariant"], re.I) else "MMFF94s" 333 334 OptionsInfo["ForceField"] = ForceField 335 OptionsInfo["MMFFVariant"] = MMFFVariant 336 OptionsInfo["UseMMFF"] = UseMMFF 337 OptionsInfo["UseUFF"] = UseUFF 338 339 if UseMMFF: 340 OptionsInfo["EnergyLabel"] = "%s_Energy" % MMFFVariant 341 else: 342 OptionsInfo["EnergyLabel"] = "%s_Energy" % ForceField 343 344 OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False 345 OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"]) 346 347 OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False 348 349 350 def RetrieveOptions(): 351 """Retrieve command line arguments and options.""" 352 353 # Get options... 354 global Options 355 Options = docopt(_docoptUsage_) 356 357 # Set current working directory to the specified directory... 358 WorkingDir = Options["--workingdir"] 359 if WorkingDir: 360 os.chdir(WorkingDir) 361 362 # Handle examples option... 363 if "--examples" in Options and Options["--examples"]: 364 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_)) 365 sys.exit(0) 366 367 368 def ValidateOptions(): 369 """Validate option values.""" 370 371 MiscUtil.ValidateOptionTextValue("-f, --forceField", Options["--forceField"], "UFF MMFF") 372 MiscUtil.ValidateOptionTextValue(" --forceFieldMMFFVariant", Options["--forceFieldMMFFVariant"], "MMFF94 MMFF94s") 373 374 MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"]) 375 MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol") 376 377 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd") 378 MiscUtil.ValidateOptionsOutputFileOverwrite( 379 "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"] 380 ) 381 MiscUtil.ValidateOptionsDistinctFileNames( 382 "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"] 383 ) 384 385 MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no") 386 MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no") 387 388 389 # Setup a usage string for docopt... 390 _docoptUsage_ = """ 391 RDKitCalculateEnergy.py - Calculate energy 392 393 Usage: 394 RDKitCalculateEnergy.py [--forceField <UFF, or MMFF>] [--forceFieldMMFFVariant <MMFF94 or MMFF94s>] 395 [--infileParams <Name,Value,...>] [--mp <yes or no>] [--mpParams <Name,Value,...>] 396 [ --outfileParams <Name,Value,...> ] [--overwrite] [--quiet <yes or no>] [-w <dir>] -i <infile> -o <outfile> 397 RDKitCalculateEnergy.py -h | --help | -e | --examples 398 399 Description: 400 Calculate single point energy for molecules using a specified forcefield. The 401 molecules must have 3D coordinates in input file. 402 403 The supported input file formats are: Mol (.mol), SD (.sdf, .sd) 404 405 The supported output file formats are: SD (.sdf, .sd) 406 407 Options: 408 -f, --forceField <UFF, MMFF> [default: MMFF] 409 Forcefield method to use for energy calculation. Possible values: Universal Force 410 Field (UFF) [ Ref 81 ] or Merck Molecular Mechanics Force Field [ Ref 83-87 ] . 411 --forceFieldMMFFVariant <MMFF94 or MMFF94s> [default: MMFF94] 412 Variant of MMFF forcefield to use for energy calculation. 413 -e, --examples 414 Print examples. 415 -h, --help 416 Print this help message. 417 -i, --infile <infile> 418 Input file name. 419 --infileParams <Name,Value,...> [default: auto] 420 A comma delimited list of parameter name and value pairs for reading 421 molecules from files. The supported parameter names for different file 422 formats, along with their default values, are shown below: 423 424 SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes 425 426 --mp <yes or no> [default: no] 427 Use multiprocessing. 428 429 By default, input data is retrieved in a lazy manner via mp.Pool.imap() 430 function employing lazy RDKit data iterable. This allows processing of 431 arbitrary large data sets without any additional requirements memory. 432 433 All input data may be optionally loaded into memory by mp.Pool.map() 434 before starting worker processes in a process pool by setting the value 435 of 'inputDataMode' to 'InMemory' in '--mpParams' option. 436 437 A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input 438 data mode may adversely impact the performance. The '--mpParams' section 439 provides additional information to tune the value of 'chunkSize'. 440 --mpParams <Name,Value,...> [default: auto] 441 A comma delimited list of parameter name and value pairs to configure 442 multiprocessing. 443 444 The supported parameter names along with their default and possible 445 values are shown below: 446 447 chunkSize, auto 448 inputDataMode, Lazy [ Possible values: InMemory or Lazy ] 449 numProcesses, auto [ Default: mp.cpu_count() ] 450 451 These parameters are used by the following functions to configure and 452 control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and 453 mp.Pool.imap(). 454 455 The chunkSize determines chunks of input data passed to each worker 456 process in a process pool by mp.Pool.map() and mp.Pool.imap() functions. 457 The default value of chunkSize is dependent on the value of 'inputDataMode'. 458 459 The mp.Pool.map() function, invoked during 'InMemory' input data mode, 460 automatically converts RDKit data iterable into a list, loads all data into 461 memory, and calculates the default chunkSize using the following method 462 as shown in its code: 463 464 chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4) 465 if extra: chunkSize += 1 466 467 For example, the default chunkSize will be 7 for a pool of 4 worker processes 468 and 100 data items. 469 470 The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs 471 'lazy' RDKit data iterable to retrieve data as needed, without loading all the 472 data into memory. Consequently, the size of input data is not known a priori. 473 It's not possible to estimate an optimal value for the chunkSize. The default 474 chunkSize is set to 1. 475 476 The default value for the chunkSize during 'Lazy' data mode may adversely 477 impact the performance due to the overhead associated with exchanging 478 small chunks of data. It is generally a good idea to explicitly set chunkSize to 479 a larger value during 'Lazy' input data mode, based on the size of your input 480 data and number of processes in the process pool. 481 482 The mp.Pool.map() function waits for all worker processes to process all 483 the data and return the results. The mp.Pool.imap() function, however, 484 returns the the results obtained from worker processes as soon as the 485 results become available for specified chunks of data. 486 487 The order of data in the results returned by both mp.Pool.map() and 488 mp.Pool.imap() functions always corresponds to the input data. 489 -o, --outfile <outfile> 490 Output file name. 491 --outfileParams <Name,Value,...> [default: auto] 492 A comma delimited list of parameter name and value pairs for writing 493 molecules to files. The supported parameter names for different file 494 formats, along with their default values, are shown below: 495 496 SD: kekulize,yes,forceV3000,no 497 498 --overwrite 499 Overwrite existing files. 500 -q, --quiet <yes or no> [default: no] 501 Use quiet mode. The warning and information messages will not be printed. 502 -w, --workingdir <dir> 503 Location of working directory which defaults to the current directory. 504 505 Examples: 506 To calculate single point energy using MMFF forcefield for molecules in a SD file 507 containing 3D structures and write a new SD file, type: 508 509 % RDKitCalculateEnergy.py -i Sample3D.sdf -o Sample3DOut.sdf 510 511 To run the first example in multiprocessing mode on all available CPUs 512 without loading all data into memory and write out a SD file, type: 513 514 % RDKitCalculateEnergy.py --mp yes -i Sample3D.sdf -o Sample3DOut.sdf 515 516 To run the first example in multiprocessing mode on all available CPUs 517 by loading all data into memory and write out a SD file, type: 518 519 % RDKitCalculateEnergy.py --mp yes --mpParams "inputDataMode, 520 InMemory" -i Sample3D.sdf -o Sample3DOut.sdf 521 522 To run the first example in multiprocessing mode on specific number of 523 CPUs and chunk size without loading all data into memory and write out a SD file, 524 type: 525 526 % RDKitCalculateEnergy.py --mp yes --mpParams "inputDataMode,Lazy, 527 numProcesses,4,chunkSize,8" -i Sample3D.sdf -o Sample3DOut.sdf 528 529 To calculate single point energy using UFF forcefield for molecules in a SD file 530 containing 3D structures and write a new SD file, type: 531 532 % RDKitCalculateEnergy.py -f UFF -i Sample3D.sdf -o Sample3DOut.sdf 533 534 To calculate single point energy using MMFF94s variant of MMFF forcefield for 535 molecules in a SD file containing 3D structures and write a new SD file, type: 536 537 % RDKitCalculateEnergy.py --forceField MMFF --forceFieldMMFFVariant 538 MMFF94s -i Sample3D.sdf -o Sample3DOut.sdf 539 540 Author: 541 Manish Sud(msud@san.rr.com) 542 543 See also: 544 RDKitCalculateRMSD.py, RDKitCalculateMolecularDescriptors.py, RDKitCompareMoleculeShapes.py, 545 RDKitConvertFileFormat.py, RDKitGenerateConformers.py, RDKitPerformMinimization.py 546 547 Copyright: 548 Copyright (C) 2026 Manish Sud. All rights reserved. 549 550 The functionality available in this script is implemented using RDKit, an 551 open source toolkit for cheminformatics developed by Greg Landrum. 552 553 This file is part of MayaChemTools. 554 555 MayaChemTools is free software; you can redistribute it and/or modify it under 556 the terms of the GNU Lesser General Public License as published by the Free 557 Software Foundation; either version 3 of the License, or (at your option) any 558 later version. 559 560 """ 561 562 if __name__ == "__main__": 563 main()