1 #!/bin/env python 2 # 3 # File: RDKitPerformMinimization.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 PerformMinimization() 81 82 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName) 83 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime)) 84 85 86 def PerformMinimization(): 87 """Perform minimization.""" 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, MinimizationFailedCount, WriteFailedCount = 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( 107 "Number of molecules failed during conformation generation or minimization: %d" % MinimizationFailedCount 108 ) 109 MiscUtil.PrintInfo("Number of molecules failed during writing: %d" % WriteFailedCount) 110 MiscUtil.PrintInfo( 111 "Number of ignored molecules: %d" % (MolCount - ValidMolCount + MinimizationFailedCount + WriteFailedCount) 112 ) 113 114 115 def ProcessMolecules(Mols, Writer): 116 """Process and minimize molecules.""" 117 118 if OptionsInfo["MPMode"]: 119 return ProcessMoleculesUsingMultipleProcesses(Mols, Writer) 120 else: 121 return ProcessMoleculesUsingSingleProcess(Mols, Writer) 122 123 124 def ProcessMoleculesUsingSingleProcess(Mols, Writer): 125 """Process and minimize molecules using a single process.""" 126 127 if OptionsInfo["SkipConformerGeneration"]: 128 MiscUtil.PrintInfo("\nPerforming minimization without generation of conformers...") 129 else: 130 MiscUtil.PrintInfo("\nPerforming minimization with generation of conformers...") 131 132 (MolCount, ValidMolCount, MinimizationFailedCount, WriteFailedCount) = [0] * 4 133 for Mol in Mols: 134 MolCount += 1 135 136 if Mol is None: 137 continue 138 139 if RDKitUtil.IsMolEmpty(Mol): 140 if not OptionsInfo["QuietMode"]: 141 MolName = RDKitUtil.GetMolName(Mol, MolCount) 142 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 143 continue 144 ValidMolCount += 1 145 146 Mol, CalcStatus, ConfID, Energy = MinimizeMoleculeOrConformers(Mol, MolCount) 147 148 if not CalcStatus: 149 MinimizationFailedCount += 1 150 continue 151 152 WriteStatus = WriteMolecule(Writer, Mol, MolCount, ConfID, Energy) 153 if not WriteStatus: 154 WriteFailedCount += 1 155 156 return (MolCount, ValidMolCount, MinimizationFailedCount, WriteFailedCount) 157 158 159 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer): 160 """Process and minimize molecules using multiprocessing.""" 161 162 if OptionsInfo["SkipConformerGeneration"]: 163 MiscUtil.PrintInfo("\nPerforming minimization without generation of conformers using multiprocessing...") 164 else: 165 MiscUtil.PrintInfo("\nPerforming minimization with generation of conformers using multiprocessing...") 166 167 MPParams = OptionsInfo["MPParams"] 168 169 # Setup data for initializing a worker process... 170 InitializeWorkerProcessArgs = ( 171 MiscUtil.ObjectToBase64EncodedString(Options), 172 MiscUtil.ObjectToBase64EncodedString(OptionsInfo), 173 ) 174 175 # Setup a encoded mols data iterable for a worker process... 176 WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols) 177 178 # Setup process pool along with data initialization for each process... 179 MiscUtil.PrintInfo( 180 "\nConfiguring multiprocessing using %s method..." 181 % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()") 182 ) 183 MiscUtil.PrintInfo( 184 "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n" 185 % ( 186 MPParams["NumProcesses"], 187 MPParams["InputDataMode"], 188 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]), 189 ) 190 ) 191 192 ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs) 193 194 # Start processing... 195 if re.match("^Lazy$", MPParams["InputDataMode"], re.I): 196 Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 197 elif re.match("^InMemory$", MPParams["InputDataMode"], re.I): 198 Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 199 else: 200 MiscUtil.PrintError( 201 'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"]) 202 ) 203 204 (MolCount, ValidMolCount, MinimizationFailedCount, WriteFailedCount) = [0] * 4 205 for Result in Results: 206 MolCount += 1 207 MolIndex, EncodedMol, CalcStatus, ConfID, Energy = Result 208 209 if EncodedMol is None: 210 continue 211 ValidMolCount += 1 212 213 if not CalcStatus: 214 MinimizationFailedCount += 1 215 continue 216 217 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 218 WriteStatus = WriteMolecule(Writer, Mol, MolCount, ConfID, Energy) 219 if not WriteStatus: 220 WriteFailedCount += 1 221 222 return (MolCount, ValidMolCount, MinimizationFailedCount, WriteFailedCount) 223 224 225 def InitializeWorkerProcess(*EncodedArgs): 226 """Initialize data for a worker process.""" 227 228 global Options, OptionsInfo 229 230 MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid()) 231 232 # Decode Options and OptionInfo... 233 Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0]) 234 OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1]) 235 236 237 def WorkerProcess(EncodedMolInfo): 238 """Process data for a worker process.""" 239 240 MolIndex, EncodedMol = EncodedMolInfo 241 242 CalcStatus = False 243 ConfID = None 244 Energy = None 245 246 if EncodedMol is None: 247 return [MolIndex, None, CalcStatus, ConfID, Energy] 248 249 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 250 if RDKitUtil.IsMolEmpty(Mol): 251 if not OptionsInfo["QuietMode"]: 252 MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1)) 253 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 254 return [MolIndex, None, CalcStatus, ConfID, Energy] 255 256 Mol, CalcStatus, ConfID, Energy = MinimizeMoleculeOrConformers(Mol, (MolIndex + 1)) 257 258 return [ 259 MolIndex, 260 RDKitUtil.MolToBase64EncodedMolString( 261 Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps 262 ), 263 CalcStatus, 264 ConfID, 265 Energy, 266 ] 267 268 269 def MinimizeMoleculeOrConformers(Mol, MolNum=None): 270 """Minimize molecule or conformers.""" 271 272 ConfID = None 273 if OptionsInfo["SkipConformerGeneration"]: 274 Mol, CalcStatus, Energy = MinimizeMolecule(Mol, MolNum) 275 else: 276 Mol, CalcStatus, ConfID, Energy = MinimizeConformers(Mol, MolNum) 277 278 return (Mol, CalcStatus, ConfID, Energy) 279 280 281 def MinimizeMolecule(Mol, MolNum=None): 282 """Minimize molecule.""" 283 284 if OptionsInfo["AddHydrogens"]: 285 Mol = Chem.AddHs(Mol, addCoords=True) 286 287 Status = 0 288 try: 289 if OptionsInfo["UseUFF"]: 290 Status = AllChem.UFFOptimizeMolecule(Mol, maxIters=OptionsInfo["MaxIters"]) 291 elif OptionsInfo["UseMMFF"]: 292 Status = AllChem.MMFFOptimizeMolecule( 293 Mol, maxIters=OptionsInfo["MaxIters"], mmffVariant=OptionsInfo["MMFFVariant"] 294 ) 295 else: 296 MiscUtil.PrintError( 297 "Minimization couldn't be performed: Specified forcefield, %s, is not supported" 298 % OptionsInfo["ForceField"] 299 ) 300 except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg: 301 if not OptionsInfo["QuietMode"]: 302 MolName = RDKitUtil.GetMolName(Mol, MolNum) 303 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg)) 304 return (Mol, False, None) 305 306 if Status != 0: 307 if not OptionsInfo["QuietMode"]: 308 MolName = RDKitUtil.GetMolName(Mol, MolNum) 309 MiscUtil.PrintWarning( 310 'Minimization failed to converge for molecule %s in %d steps. Try using higher value for "--maxIters" option...\n' 311 % (MolName, OptionsInfo["MaxIters"]) 312 ) 313 314 Energy = None 315 if OptionsInfo["EnergyOut"]: 316 EnergyStatus, Energy = GetEnergy(Mol) 317 if EnergyStatus: 318 Energy = "%.2f" % Energy 319 else: 320 if not OptionsInfo["QuietMode"]: 321 MolName = RDKitUtil.GetMolName(Mol, MolNum) 322 MiscUtil.PrintWarning( 323 "Failed to retrieve calculated energy for molecule %s. Try again after removing any salts or cleaing up the molecule...\n" 324 % (MolName) 325 ) 326 327 if OptionsInfo["RemoveHydrogens"]: 328 Mol = Chem.RemoveHs(Mol) 329 330 return (Mol, True, Energy) 331 332 333 def MinimizeConformers(Mol, MolNum=None): 334 """Generate and minimize conformers for a molecule to get the lowest energy conformer.""" 335 336 if OptionsInfo["AddHydrogens"]: 337 Mol = Chem.AddHs(Mol) 338 339 ConfIDs = EmbedMolecule(Mol, MolNum) 340 if not len(ConfIDs): 341 if not OptionsInfo["QuietMode"]: 342 MolName = RDKitUtil.GetMolName(Mol, MolNum) 343 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s: Embedding failed...\n" % MolName) 344 return (Mol, False, None, None) 345 346 CalcEnergyMap = {} 347 for ConfID in ConfIDs: 348 try: 349 if OptionsInfo["UseUFF"]: 350 Status = AllChem.UFFOptimizeMolecule(Mol, confId=ConfID, maxIters=OptionsInfo["MaxIters"]) 351 elif OptionsInfo["UseMMFF"]: 352 Status = AllChem.MMFFOptimizeMolecule( 353 Mol, confId=ConfID, maxIters=OptionsInfo["MaxIters"], mmffVariant=OptionsInfo["MMFFVariant"] 354 ) 355 else: 356 MiscUtil.PrintError( 357 "Minimization couldn't be performed: Specified forcefield, %s, is not supported" 358 % OptionsInfo["ForceField"] 359 ) 360 except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg: 361 if not OptionsInfo["QuietMode"]: 362 MolName = RDKitUtil.GetMolName(Mol, MolNum) 363 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg)) 364 return (Mol, False, None, None) 365 366 EnergyStatus, Energy = GetEnergy(Mol, ConfID) 367 if not EnergyStatus: 368 if not OptionsInfo["QuietMode"]: 369 MolName = RDKitUtil.GetMolName(Mol, MolNum) 370 MiscUtil.PrintWarning( 371 "Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n" 372 % (ConfID, MolName) 373 ) 374 return (Mol, False, None, None) 375 376 if Status != 0: 377 if not OptionsInfo["QuietMode"]: 378 MolName = RDKitUtil.GetMolName(Mol, MolNum) 379 MiscUtil.PrintWarning( 380 'Minimization failed to converge for conformation number %d of molecule %s in %d steps. Try using higher value for "--maxIters" option...\n' 381 % (ConfID, MolName, OptionsInfo["MaxIters"]) 382 ) 383 384 CalcEnergyMap[ConfID] = Energy 385 386 SortedConfIDs = sorted(ConfIDs, key=lambda ConfID: CalcEnergyMap[ConfID]) 387 MinEnergyConfID = SortedConfIDs[0] 388 389 if OptionsInfo["RemoveHydrogens"]: 390 Mol = Chem.RemoveHs(Mol) 391 392 Energy = "%.2f" % CalcEnergyMap[MinEnergyConfID] if OptionsInfo["EnergyOut"] else None 393 394 return (Mol, True, MinEnergyConfID, Energy) 395 396 397 def GetEnergy(Mol, ConfID=None): 398 """Calculate energy.""" 399 400 Status = True 401 Energy = None 402 403 if ConfID is None: 404 ConfID = -1 405 406 if OptionsInfo["UseUFF"]: 407 UFFMoleculeForcefield = AllChem.UFFGetMoleculeForceField(Mol, confId=ConfID) 408 if UFFMoleculeForcefield is None: 409 Status = False 410 else: 411 Energy = UFFMoleculeForcefield.CalcEnergy() 412 elif OptionsInfo["UseMMFF"]: 413 MMFFMoleculeProperties = AllChem.MMFFGetMoleculeProperties(Mol, mmffVariant=OptionsInfo["MMFFVariant"]) 414 MMFFMoleculeForcefield = AllChem.MMFFGetMoleculeForceField(Mol, MMFFMoleculeProperties, confId=ConfID) 415 if MMFFMoleculeForcefield is None: 416 Status = False 417 else: 418 Energy = MMFFMoleculeForcefield.CalcEnergy() 419 else: 420 MiscUtil.PrintError( 421 "Couldn't retrieve conformer energy: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"] 422 ) 423 424 return (Status, Energy) 425 426 427 def EmbedMolecule(Mol, MolNum=None): 428 """Embed conformations.""" 429 430 ConfIDs = [] 431 432 MaxConfs = OptionsInfo["MaxConfs"] 433 RandomSeed = OptionsInfo["RandomSeed"] 434 EnforceChirality = OptionsInfo["EnforceChirality"] 435 UseExpTorsionAnglePrefs = OptionsInfo["UseExpTorsionAnglePrefs"] 436 ETVersion = OptionsInfo["ETVersion"] 437 UseBasicKnowledge = OptionsInfo["UseBasicKnowledge"] 438 439 try: 440 ConfIDs = AllChem.EmbedMultipleConfs( 441 Mol, 442 numConfs=MaxConfs, 443 randomSeed=RandomSeed, 444 enforceChirality=EnforceChirality, 445 useExpTorsionAnglePrefs=UseExpTorsionAnglePrefs, 446 useBasicKnowledge=UseBasicKnowledge, 447 ETversion=ETVersion, 448 ) 449 except ValueError as ErrMsg: 450 if not OptionsInfo["QuietMode"]: 451 MolName = RDKitUtil.GetMolName(Mol, MolNum) 452 MiscUtil.PrintWarning("Embedding failed for molecule %s:\n%s\n" % (MolName, ErrMsg)) 453 ConfIDs = [] 454 455 return ConfIDs 456 457 458 def WriteMolecule(Writer, Mol, MolNum=None, ConfID=None, Energy=None): 459 """Write molecule.""" 460 461 if Energy is not None: 462 Mol.SetProp(OptionsInfo["EnergyLabel"], Energy) 463 464 try: 465 if ConfID is None: 466 Writer.write(Mol) 467 else: 468 Writer.write(Mol, confId=ConfID) 469 except (ValueError, RuntimeError) as ErrMsg: 470 if not OptionsInfo["QuietMode"]: 471 MolName = RDKitUtil.GetMolName(Mol, MolNum) 472 MiscUtil.PrintWarning("Failed to write molecule %s:\n%s\n" % (MolName, ErrMsg)) 473 return False 474 475 return True 476 477 478 def ProcesssConformerGeneratorOption(): 479 """Process comformer generator option.""" 480 481 ConfGenParams = MiscUtil.ProcessOptionConformerGenerator("--conformerGenerator", Options["--conformerGenerator"]) 482 483 OptionsInfo["ConformerGenerator"] = ConfGenParams["ConformerGenerator"] 484 OptionsInfo["SkipConformerGeneration"] = ConfGenParams["SkipConformerGeneration"] 485 OptionsInfo["UseBasicKnowledge"] = ConfGenParams["UseBasicKnowledge"] 486 OptionsInfo["UseExpTorsionAnglePrefs"] = ConfGenParams["UseExpTorsionAnglePrefs"] 487 OptionsInfo["ETVersion"] = ConfGenParams["ETVersion"] 488 489 490 def ProcessOptions(): 491 """Process and validate command line arguments and options.""" 492 493 MiscUtil.PrintInfo("Processing options...") 494 495 # Validate options... 496 ValidateOptions() 497 498 OptionsInfo["Infile"] = Options["--infile"] 499 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters( 500 "--infileParams", Options["--infileParams"], Options["--infile"] 501 ) 502 503 OptionsInfo["Outfile"] = Options["--outfile"] 504 OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters( 505 "--outfileParams", Options["--outfileParams"] 506 ) 507 508 OptionsInfo["Overwrite"] = Options["--overwrite"] 509 510 OptionsInfo["AddHydrogens"] = True if re.match("^yes$", Options["--addHydrogens"], re.I) else False 511 512 ProcesssConformerGeneratorOption() 513 514 if re.match("^UFF$", Options["--forceField"], re.I): 515 ForceField = "UFF" 516 UseUFF = True 517 UseMMFF = False 518 elif re.match("^MMFF$", Options["--forceField"], re.I): 519 ForceField = "MMFF" 520 UseUFF = False 521 UseMMFF = True 522 else: 523 MiscUtil.PrintError( 524 'The value, %s, specified for "--forceField" is not supported.' % (Options["--forceField"],) 525 ) 526 527 MMFFVariant = "MMFF94" if re.match("^MMFF94$", Options["--forceFieldMMFFVariant"], re.I) else "MMFF94s" 528 529 OptionsInfo["ForceField"] = ForceField 530 OptionsInfo["MMFFVariant"] = MMFFVariant 531 OptionsInfo["UseMMFF"] = UseMMFF 532 OptionsInfo["UseUFF"] = UseUFF 533 534 OptionsInfo["EnergyOut"] = True if re.match("^yes$", Options["--energyOut"], re.I) else False 535 if UseMMFF: 536 OptionsInfo["EnergyLabel"] = "%s_Energy" % MMFFVariant 537 else: 538 OptionsInfo["EnergyLabel"] = "%s_Energy" % ForceField 539 540 OptionsInfo["EnforceChirality"] = True if re.match("^yes$", Options["--enforceChirality"], re.I) else False 541 542 OptionsInfo["MaxIters"] = int(Options["--maxIters"]) 543 OptionsInfo["MaxConfs"] = int(Options["--maxConfs"]) 544 545 OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False 546 OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"]) 547 548 OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False 549 550 RandomSeed = -1 551 if not re.match("^auto$", Options["--randomSeed"], re.I): 552 RandomSeed = int(Options["--randomSeed"]) 553 OptionsInfo["RandomSeed"] = RandomSeed 554 555 OptionsInfo["RemoveHydrogens"] = True if re.match("^yes$", Options["--removeHydrogens"], re.I) else False 556 557 558 def RetrieveOptions(): 559 """Retrieve command line arguments and options.""" 560 561 # Get options... 562 global Options 563 Options = docopt(_docoptUsage_) 564 565 # Set current working directory to the specified directory... 566 WorkingDir = Options["--workingdir"] 567 if WorkingDir: 568 os.chdir(WorkingDir) 569 570 # Handle examples option... 571 if "--examples" in Options and Options["--examples"]: 572 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_)) 573 sys.exit(0) 574 575 576 def ValidateOptions(): 577 """Validate option values.""" 578 579 MiscUtil.ValidateOptionTextValue("-a, --addHydrogens", Options["--addHydrogens"], "yes no") 580 MiscUtil.ValidateOptionTextValue( 581 "-c, --conformerGenerator", Options["--conformerGenerator"], "SDG ETDG KDG ETKDG ETKDGv2 None" 582 ) 583 584 MiscUtil.ValidateOptionTextValue("-f, --forceField", Options["--forceField"], "UFF MMFF") 585 MiscUtil.ValidateOptionTextValue(" --forceFieldMMFFVariant", Options["--forceFieldMMFFVariant"], "MMFF94 MMFF94s") 586 587 MiscUtil.ValidateOptionTextValue("--energyOut", Options["--energyOut"], "yes no") 588 MiscUtil.ValidateOptionTextValue("--enforceChirality ", Options["--enforceChirality"], "yes no") 589 590 MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"]) 591 MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv") 592 593 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd") 594 MiscUtil.ValidateOptionsOutputFileOverwrite( 595 "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"] 596 ) 597 MiscUtil.ValidateOptionsDistinctFileNames( 598 "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"] 599 ) 600 601 MiscUtil.ValidateOptionIntegerValue("--maxConfs", Options["--maxConfs"], {">": 0}) 602 MiscUtil.ValidateOptionIntegerValue("--maxIters", Options["--maxIters"], {">": 0}) 603 604 MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no") 605 MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no") 606 607 if not re.match("^auto$", Options["--randomSeed"], re.I): 608 MiscUtil.ValidateOptionIntegerValue("--randomSeed", Options["--randomSeed"], {}) 609 610 MiscUtil.ValidateOptionTextValue("-r, --removeHydrogens", Options["--removeHydrogens"], "yes no") 611 612 613 # Setup a usage string for docopt... 614 _docoptUsage_ = """ 615 RDKitPerformMinimization.py - Perform structure minimization 616 617 Usage: 618 RDKitPerformMinimization.py [--addHydrogens <yes or no>] [--conformerGenerator <SDG, KDG, ETDG, ETKDG, ETKDGv2, None>] 619 [--forceField <UFF, or MMFF>] [--forceFieldMMFFVariant <MMFF94 or MMFF94s>] 620 [--energyOut <yes or no>] [--enforceChirality <yes or no>] [--infileParams <Name,Value,...>] 621 [--maxConfs <number>] [--maxIters <number>] [--mp <yes or no>] [--mpParams <Name,Value,...>] 622 [ --outfileParams <Name,Value,...> ] [--overwrite] [--quiet <yes or no>] [ --removeHydrogens <yes or no>] 623 [--randomSeed <number>] [-w <dir>] -i <infile> -o <outfile> 624 RDKitPerformMinimization.py -h | --help | -e | --examples 625 626 Description: 627 Generate 3D structures for molecules using a combination of distance geometry 628 and forcefield minimization or minimize existing 3D structures using a specified 629 forcefield. 630 631 The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi 632 .csv, .tsv, .txt) 633 634 The supported output file formats are: SD (.sdf, .sd) 635 636 Options: 637 -a, --addHydrogens <yes or no> [default: yes] 638 Add hydrogens before minimization. 639 -c, --conformerGenerator <text or None> [default: ETKDGv2] 640 Conformation generation methodology for generating initial 3D coordinates. The 641 possible values along with a brief description are shown below: 642 643 SDG: Standard Distance Geometry 644 KDG: basic Knowledge-terms with Distance Geometry 645 ETDG: Experimental Torsion-angle preference with Distance Geometry 646 ETKDG: Experimental Torsion-angle preference along with basic 647 Knowledge-terms and Distance Geometry [Ref 129] 648 ETKDGv2: Experimental Torsion-angle preference along with basic 649 Knowledge-terms and Distance Geometry [Ref 167] 650 None: No conformation generation 651 652 The conformation generation step may be skipped by specifying 'None' value to 653 perform only forcefield minimization of molecules with 3D structures in input 654 file. This doesn't work for molecules in SMILES file or molecules in SD/MOL files 655 containing 2D structures. 656 -f, --forceField <UFF, MMFF> [default: MMFF] 657 Forcefield method to use for energy minimization. Possible values: Universal Force 658 Field (UFF) [ Ref 81 ] or Merck Molecular Mechanics Force Field [ Ref 83-87 ] . 659 --forceFieldMMFFVariant <MMFF94 or MMFF94s> [default: MMFF94] 660 Variant of MMFF forcefield to use for energy minimization. 661 --energyOut <yes or no> [default: No] 662 Write out energy values. 663 --enforceChirality <yes or no> [default: Yes] 664 Enforce chirality for defined chiral centers. 665 -e, --examples 666 Print examples. 667 -h, --help 668 Print this help message. 669 -i, --infile <infile> 670 Input file name. 671 --infileParams <Name,Value,...> [default: auto] 672 A comma delimited list of parameter name and value pairs for reading 673 molecules from files. The supported parameter names for different file 674 formats, along with their default values, are shown below: 675 676 SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes 677 SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space, 678 smilesTitleLine,auto,sanitize,yes 679 680 Possible values for smilesDelimiter: space, comma or tab. 681 --maxConfs <number> [default: 250] 682 Maximum number of conformations to generate for each molecule by conformation 683 generation methodology for initial 3D coordinates. The conformations are minimized 684 using the specified forcefield and the lowest energy conformation is written to the 685 output file. This option is ignored during 'None' value of '-c --conformerGenerator' 686 option. 687 --maxIters <number> [default: 500] 688 Maximum number of iterations to perform for each molecule during forcefield 689 minimization. 690 --mp <yes or no> [default: no] 691 Use multiprocessing. 692 693 By default, input data is retrieved in a lazy manner via mp.Pool.imap() 694 function employing lazy RDKit data iterable. This allows processing of 695 arbitrary large data sets without any additional requirements memory. 696 697 All input data may be optionally loaded into memory by mp.Pool.map() 698 before starting worker processes in a process pool by setting the value 699 of 'inputDataMode' to 'InMemory' in '--mpParams' option. 700 701 A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input 702 data mode may adversely impact the performance. The '--mpParams' section 703 provides additional information to tune the value of 'chunkSize'. 704 --mpParams <Name,Value,...> [default: auto] 705 A comma delimited list of parameter name and value pairs to configure 706 multiprocessing. 707 708 The supported parameter names along with their default and possible 709 values are shown below: 710 711 chunkSize, auto 712 inputDataMode, Lazy [ Possible values: InMemory or Lazy ] 713 numProcesses, auto [ Default: mp.cpu_count() ] 714 715 These parameters are used by the following functions to configure and 716 control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and 717 mp.Pool.imap(). 718 719 The chunkSize determines chunks of input data passed to each worker 720 process in a process pool by mp.Pool.map() and mp.Pool.imap() functions. 721 The default value of chunkSize is dependent on the value of 'inputDataMode'. 722 723 The mp.Pool.map() function, invoked during 'InMemory' input data mode, 724 automatically converts RDKit data iterable into a list, loads all data into 725 memory, and calculates the default chunkSize using the following method 726 as shown in its code: 727 728 chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4) 729 if extra: chunkSize += 1 730 731 For example, the default chunkSize will be 7 for a pool of 4 worker processes 732 and 100 data items. 733 734 The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs 735 'lazy' RDKit data iterable to retrieve data as needed, without loading all the 736 data into memory. Consequently, the size of input data is not known a priori. 737 It's not possible to estimate an optimal value for the chunkSize. The default 738 chunkSize is set to 1. 739 740 The default value for the chunkSize during 'Lazy' data mode may adversely 741 impact the performance due to the overhead associated with exchanging 742 small chunks of data. It is generally a good idea to explicitly set chunkSize to 743 a larger value during 'Lazy' input data mode, based on the size of your input 744 data and number of processes in the process pool. 745 746 The mp.Pool.map() function waits for all worker processes to process all 747 the data and return the results. The mp.Pool.imap() function, however, 748 returns the the results obtained from worker processes as soon as the 749 results become available for specified chunks of data. 750 751 The order of data in the results returned by both mp.Pool.map() and 752 mp.Pool.imap() functions always corresponds to the input data. 753 -o, --outfile <outfile> 754 Output file name. 755 --outfileParams <Name,Value,...> [default: auto] 756 A comma delimited list of parameter name and value pairs for writing 757 molecules to files. The supported parameter names for different file 758 formats, along with their default values, are shown below: 759 760 SD: kekulize,yes,forceV3000,no 761 762 --overwrite 763 Overwrite existing files. 764 -q, --quiet <yes or no> [default: no] 765 Use quiet mode. The warning and information messages will not be printed. 766 -r, --removeHydrogens <yes or no> [default: Yes] 767 Remove hydrogens after minimization. 768 --randomSeed <number> [default: auto] 769 Seed for the random number generator for reproducing 3D coordinates. 770 Default is to use a random seed. 771 -w, --workingdir <dir> 772 Location of working directory which defaults to the current directory. 773 774 Examples: 775 To generate up to 250 conformations using ETKDG methodology followed by MMFF 776 forcefield minimization for a maximum of 500 iterations for molecules in a SMILES file 777 and write out a SD file containing minimum energy structure corresponding to each 778 molecule, type: 779 780 % RDKitPerformMinimization.py -i Sample.smi -o SampleOut.sdf 781 782 To rerun the first example in a quiet mode and write out a SD file, type: 783 784 % RDKitPerformMinimization.py -q yes -i Sample.smi -o SampleOut.sdf 785 786 To run the first example in multiprocessing mode on all available CPUs 787 without loading all data into memory and write out a SD file, type: 788 789 % RDKitPerformMinimization.py --mp yes -i Sample.smi -o SampleOut.sdf 790 791 To run the first example in multiprocessing mode on all available CPUs 792 by loading all data into memory and write out a SD file, type: 793 794 % RDKitPerformMinimization.py --mp yes --mpParams "inputDataMode, 795 InMemory" -i Sample.smi -o SampleOut.sdf 796 797 To run the first example in multiprocessing mode on specific number of 798 CPUs and chunk size without loading all data into memory and write out a SD file, 799 type: 800 801 % RDKitPerformMinimization.py --mp yes --mpParams "inputDataMode,Lazy, 802 numProcesses,4,chunkSize,8" -i Sample.smi -o SampleOut.sdf 803 804 To generate up to 150 conformations using ETKDG methodology followed by MMFF 805 forcefield minimization for a maximum of 250 iterations along with a specified random 806 seed for molecules in a SMILES file and write out a SD file containing minimum energy 807 structures corresponding to each molecule, type 808 809 % RDKitPerformMinimization.py --maxConfs 150 --randomSeed 201780117 810 --maxIters 250 -i Sample.smi -o SampleOut.sdf 811 812 To minimize structures in a 3D SD file using UFF forcefield for a maximum of 150 813 iterations without generating any conformations and write out a SD file containing 814 minimum energy structures corresponding to each molecule, type 815 816 % RDKitPerformMinimization.py -c None -f UFF --maxIters 150 817 -i Sample3D.sdf -o SampleOut.sdf 818 819 To generate up to 50 conformations using SDG methodology followed 820 by UFF forcefield minimization for a maximum of 50 iterations for 821 molecules in a CSV SMILES file, SMILES strings in column 1, name in 822 column 2, and write out a SD file, type: 823 824 % RDKitPerformMinimization.py --maxConfs 50 --maxIters 50 -c SDG 825 -f UFF --infileParams "smilesDelimiter,comma,smilesTitleLine,yes, 826 smilesColumn,1,smilesNameColumn,2" -i SampleSMILES.csv 827 -o SampleOut.sdf 828 829 Author: 830 Manish Sud(msud@san.rr.com) 831 832 See also: 833 RDKitCalculateRMSD.py, RDKitCalculateMolecularDescriptors.py, RDKitCompareMoleculeShapes.py, 834 RDKitConvertFileFormat.py, RDKitGenerateConformers.py, RDKitPerformConstrainedMinimization.py 835 836 Copyright: 837 Copyright (C) 2026 Manish Sud. All rights reserved. 838 839 The functionality available in this script is implemented using RDKit, an 840 open source toolkit for cheminformatics developed by Greg Landrum. 841 842 This file is part of MayaChemTools. 843 844 MayaChemTools is free software; you can redistribute it and/or modify it under 845 the terms of the GNU Lesser General Public License as published by the Free 846 Software Foundation; either version 3 of the License, or (at your option) any 847 later version. 848 849 """ 850 851 if __name__ == "__main__": 852 main()