1 #!/bin/env python 2 # 3 # File: RDKitPerformTorsionScan.py 4 # Author: Manish Sud <msud@san.rr.com> 5 # 6 # Copyright (C) 2024 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 # Add local python path to the global path and import standard library modules... 32 import os 33 import sys; sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python")) 34 import time 35 import re 36 import glob 37 import multiprocessing as mp 38 39 import matplotlib.pyplot as plt 40 import seaborn as sns 41 42 # RDKit imports... 43 try: 44 from rdkit import rdBase 45 from rdkit import Chem 46 from rdkit.Chem import AllChem 47 from rdkit.Chem import rdMolTransforms 48 except ImportError as ErrMsg: 49 sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg) 50 sys.stderr.write("Check/update your RDKit environment and try again.\n\n") 51 sys.exit(1) 52 53 # MayaChemTools imports... 54 try: 55 from docopt import docopt 56 import MiscUtil 57 import RDKitUtil 58 except ImportError as ErrMsg: 59 sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg) 60 sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n") 61 sys.exit(1) 62 63 ScriptName = os.path.basename(sys.argv[0]) 64 Options = {} 65 OptionsInfo = {} 66 67 def main(): 68 """Start execution of the script.""" 69 70 MiscUtil.PrintInfo("\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n" % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())) 71 72 (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime() 73 74 # Retrieve command line arguments and options... 75 RetrieveOptions() 76 77 # Process and validate command line arguments and options... 78 ProcessOptions() 79 80 # Perform actions required by the script... 81 PerformTorsionScan() 82 83 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName) 84 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime)) 85 86 def PerformTorsionScan(): 87 """Perform torsion scan.""" 88 89 # Setup a molecule reader for input file... 90 MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"]) 91 OptionsInfo["InfileParams"]["AllowEmptyMols"] = True 92 Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"]) 93 94 PlotExt = OptionsInfo["OutPlotParams"]["OutExt"] 95 FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"]) 96 MiscUtil.PrintInfo("Generating output files %s_*.sdf, %s_*Torsion*Match*.sdf, %s_*Torsion*Match*Energies.csv, %s_*Torsion*Match*Plot.%s..." % (FileName, FileName, FileName, FileName, PlotExt)) 97 98 MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount = ProcessMolecules(Mols) 99 100 MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount) 101 MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount) 102 MiscUtil.PrintInfo("Number of molecules failed during initial minimization: %d" % MinimizationFailedCount) 103 MiscUtil.PrintInfo("Number of molecules without any matched torsions: %d" % TorsionsMissingCount) 104 MiscUtil.PrintInfo("Number of molecules failed during torsion scan: %d" % TorsionsScanFailedCount) 105 MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + TorsionsMissingCount + MinimizationFailedCount + TorsionsScanFailedCount)) 106 107 def ProcessMolecules(Mols): 108 """Process molecules to perform torsion scan.""" 109 110 if OptionsInfo["MPMode"]: 111 return ProcessMoleculesUsingMultipleProcesses(Mols) 112 else: 113 return ProcessMoleculesUsingSingleProcess( Mols) 114 115 def ProcessMoleculesUsingSingleProcess(Mols): 116 """Process molecules to perform torsion scan using a single process.""" 117 118 MolInfoText = "first molecule" 119 if not OptionsInfo["FirstMolMode"]: 120 MolInfoText = "all molecules" 121 122 if OptionsInfo["TorsionMinimize"]: 123 MiscUtil.PrintInfo("\nPerforming torsion scan on %s by generating conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..." % (MolInfoText)) 124 else: 125 MiscUtil.PrintInfo("\nPerforming torsion scan on %s by skipping generation of conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..." % (MolInfoText)) 126 127 SetupTorsionsPatternsInfo() 128 129 (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5 130 131 for Mol in Mols: 132 MolCount += 1 133 134 if OptionsInfo["FirstMolMode"] and MolCount > 1: 135 MolCount -= 1 136 break 137 138 if Mol is None: 139 continue 140 141 if RDKitUtil.IsMolEmpty(Mol): 142 if not OptionsInfo["QuietMode"]: 143 MolName = RDKitUtil.GetMolName(Mol, MolCount) 144 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 145 continue 146 ValidMolCount += 1 147 148 Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(Mol, MolCount) 149 150 if not MinimizationCalcStatus: 151 MinimizationFailedCount += 1 152 continue 153 154 if not TorsionsMatchStatus: 155 TorsionsMissingCount += 1 156 continue 157 158 if not TorsionsScanCalcStatus: 159 TorsionsScanFailedCount += 1 160 continue 161 162 return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount) 163 164 def ProcessMoleculesUsingMultipleProcesses(Mols): 165 """Process and minimize molecules using multiprocessing.""" 166 167 if OptionsInfo["MPLevelTorsionAnglesMode"]: 168 return ProcessMoleculesUsingMultipleProcessesAtTorsionAnglesLevel(Mols) 169 elif OptionsInfo["MPLevelMoleculesMode"]: 170 return ProcessMoleculesUsingMultipleProcessesAtMoleculesLevel(Mols) 171 else: 172 MiscUtil.PrintError("The value, %s, option \"--mpLevel\" is not supported." % (OptionsInfo["MPLevel"])) 173 174 def ProcessMoleculesUsingMultipleProcessesAtMoleculesLevel(Mols): 175 """Process molecules to perform torsion scan using multiprocessing at molecules level.""" 176 177 MolInfoText = "first molecule" 178 if not OptionsInfo["FirstMolMode"]: 179 MolInfoText = "all molecules" 180 181 if OptionsInfo["TorsionMinimize"]: 182 MiscUtil.PrintInfo("\nPerforming torsion scan on %s using multiprocessing at molecules level by generating conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..." % (MolInfoText)) 183 else: 184 MiscUtil.PrintInfo("\nPerforming torsion scan %s using multiprocessing at molecules level by skipping generation of conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..." % (MolInfoText)) 185 186 MPParams = OptionsInfo["MPParams"] 187 188 # Setup data for initializing a worker process... 189 MiscUtil.PrintInfo("\nEncoding options info...") 190 191 InitializeWorkerProcessArgs = (MiscUtil.ObjectToBase64EncodedString(Options), MiscUtil.ObjectToBase64EncodedString(OptionsInfo)) 192 193 if OptionsInfo["FirstMolMode"]: 194 Mol = Mols[0] 195 Mols = [Mol] 196 197 # Setup a encoded mols data iterable for a worker process... 198 WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols) 199 200 # Setup process pool along with data initialization for each process... 201 MiscUtil.PrintInfo("\nConfiguring multiprocessing using %s method..." % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")) 202 MiscUtil.PrintInfo("NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n" % (MPParams["NumProcesses"], MPParams["InputDataMode"], ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]))) 203 204 ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs) 205 206 # Start processing... 207 if re.match("^Lazy$", MPParams["InputDataMode"], re.I): 208 Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 209 elif re.match("^InMemory$", MPParams["InputDataMode"], re.I): 210 Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 211 else: 212 MiscUtil.PrintError("The value, %s, specified for \"--inputDataMode\" is not supported." % (MPParams["InputDataMode"])) 213 214 (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5 215 216 for Result in Results: 217 MolCount += 1 218 219 MolIndex, EncodedMol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = Result 220 221 if EncodedMol is None: 222 continue 223 ValidMolCount += 1 224 225 if not MinimizationCalcStatus: 226 MinimizationFailedCount += 1 227 continue 228 229 if not TorsionsMatchStatus: 230 TorsionsMissingCount += 1 231 continue 232 233 if not TorsionsScanCalcStatus: 234 TorsionsScanFailedCount += 1 235 continue 236 237 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 238 239 return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount) 240 241 def InitializeWorkerProcess(*EncodedArgs): 242 """Initialize data for a worker process.""" 243 244 global Options, OptionsInfo 245 246 MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid()) 247 248 # Decode Options and OptionInfo... 249 Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0]) 250 OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1]) 251 252 # Initialize torsion patterns info... 253 SetupTorsionsPatternsInfo() 254 255 def WorkerProcess(EncodedMolInfo): 256 """Process data for a worker process.""" 257 258 MolIndex, EncodedMol = EncodedMolInfo 259 260 (MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus) = [False] * 3 261 262 if EncodedMol is None: 263 return [MolIndex, None, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus] 264 265 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 266 if RDKitUtil.IsMolEmpty(Mol): 267 if not OptionsInfo["QuietMode"]: 268 MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1)) 269 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 270 return [MolIndex, None, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus] 271 272 Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(Mol, (MolIndex + 1)) 273 274 return [MolIndex, RDKitUtil.MolToBase64EncodedMolString(Mol, PropertyPickleFlags = Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps), MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus] 275 276 def ProcessMoleculesUsingMultipleProcessesAtTorsionAnglesLevel(Mols): 277 """Process molecules to perform torsion scan using multiprocessing at torsion angles level.""" 278 279 MolInfoText = "first molecule" 280 if not OptionsInfo["FirstMolMode"]: 281 MolInfoText = "all molecules" 282 283 if OptionsInfo["TorsionMinimize"]: 284 MiscUtil.PrintInfo("\nPerforming torsion scan on %s using multiprocessing at torsion angles level by generating conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..." % (MolInfoText)) 285 else: 286 MiscUtil.PrintInfo("\nPerforming torsion scan %s using multiprocessing at torsion angles level by skipping generation of conformation ensembles for specific torsion angles and constrained energy minimization of the ensembles..." % (MolInfoText)) 287 288 SetupTorsionsPatternsInfo() 289 290 (MolCount, ValidMolCount, TorsionsMissingCount, MinimizationFailedCount, TorsionsScanFailedCount) = [0] * 5 291 292 for Mol in Mols: 293 MolCount += 1 294 295 if OptionsInfo["FirstMolMode"] and MolCount > 1: 296 MolCount -= 1 297 break 298 299 if Mol is None: 300 continue 301 302 if RDKitUtil.IsMolEmpty(Mol): 303 if not OptionsInfo["QuietMode"]: 304 MolName = RDKitUtil.GetMolName(Mol, MolCount) 305 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName) 306 continue 307 ValidMolCount += 1 308 309 Mol, MinimizationCalcStatus, TorsionsMatchStatus, TorsionsScanCalcStatus = PerformMinimizationAndTorsionScan(Mol, MolCount, UseMultiProcessingAtTorsionAnglesLevel = True) 310 311 if not MinimizationCalcStatus: 312 MinimizationFailedCount += 1 313 continue 314 315 if not TorsionsMatchStatus: 316 TorsionsMissingCount += 1 317 continue 318 319 if not TorsionsScanCalcStatus: 320 TorsionsScanFailedCount += 1 321 continue 322 323 return (MolCount, ValidMolCount, MinimizationFailedCount, TorsionsMissingCount, TorsionsScanFailedCount) 324 325 def ScanSingleTorsionInMolUsingMultipleProcessesAtTorsionAnglesLevel(Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum): 326 """Perform torsion scan for a molecule using multiple processses at torsion angles 327 level along with constrained energy minimization. 328 """ 329 330 if OptionsInfo["MPLevelMoleculesMode"]: 331 MiscUtil.PrintError("Single torison scanning for a molecule is not allowed in multiprocessing mode at molecules level.\n") 332 333 Mols, Angles = SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum) 334 335 MPParams = OptionsInfo["MPParams"] 336 337 # Setup data for initializing a worker process... 338 MiscUtil.PrintInfo("\nEncoding options info...") 339 340 # Track and avoid encoding TorsionsPatternsInfo as it contains RDKit molecule object... 341 TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"] 342 OptionsInfo["TorsionsPatternsInfo"] = None 343 344 InitializeWorkerProcessArgs = (MiscUtil.ObjectToBase64EncodedString(Options), MiscUtil.ObjectToBase64EncodedString(OptionsInfo)) 345 346 # Restore TorsionsPatternsInfo... 347 OptionsInfo["TorsionsPatternsInfo"] = TorsionsPatternsInfo 348 349 # Setup a encoded mols data iterable for a worker process... 350 WorkerProcessDataIterable = GenerateBase64EncodedMolStringsWithTorsionScanInfo(Mol, (MolNum -1), Mols, Angles, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches) 351 352 # Setup process pool along with data initialization for each process... 353 MiscUtil.PrintInfo("\nConfiguring multiprocessing using %s method..." % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")) 354 MiscUtil.PrintInfo("NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n" % (MPParams["NumProcesses"], MPParams["InputDataMode"], ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]))) 355 356 ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeTorsionAngleWorkerProcess, InitializeWorkerProcessArgs) 357 358 # Start processing... 359 if re.match("^Lazy$", MPParams["InputDataMode"], re.I): 360 Results = ProcessPool.imap(TorsionAngleWorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 361 elif re.match("^InMemory$", MPParams["InputDataMode"], re.I): 362 Results = ProcessPool.map(TorsionAngleWorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"]) 363 else: 364 MiscUtil.PrintError("The value, %s, specified for \"--inputDataMode\" is not supported." % (MPParams["InputDataMode"])) 365 366 TorsionMols = [] 367 TorsionEnergies = [] 368 TorsionAngles = [] 369 370 for Result in Results: 371 EncodedTorsionMol, CalcStatus, Angle, Energy = Result 372 373 if not CalcStatus: 374 return (Mol, False, None, None, None) 375 376 if EncodedTorsionMol is None: 377 return (Mol, False, None, None, None) 378 TorsionMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionMol) 379 380 if OptionsInfo["RemoveHydrogens"]: 381 TorsionMol = Chem.RemoveHs(TorsionMol) 382 383 TorsionMols.append(TorsionMol) 384 TorsionEnergies.append(Energy) 385 TorsionAngles.append(Angle) 386 387 return (Mol, True, TorsionMols, TorsionEnergies, TorsionAngles) 388 389 def InitializeTorsionAngleWorkerProcess(*EncodedArgs): 390 """Initialize data for a worker process.""" 391 392 global Options, OptionsInfo 393 394 MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid()) 395 396 # Decode Options and OptionInfo... 397 Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0]) 398 OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1]) 399 400 # Initialize torsion patterns info... 401 SetupTorsionsPatternsInfo() 402 403 def TorsionAngleWorkerProcess(EncodedMolInfo): 404 """Process data for a worker process.""" 405 406 MolIndex, EncodedMol, EncodedTorsionMol, TorsionAngle, TorsionID, TorsionPattern, EncodedTorsionPatternMol, TorsionMatches = EncodedMolInfo 407 408 if EncodedMol is None or EncodedTorsionMol is None or EncodedTorsionPatternMol is None: 409 return (None, False, None, None) 410 411 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol) 412 TorsionMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionMol) 413 TorsionPatternMol = RDKitUtil.MolFromBase64EncodedMolString(EncodedTorsionPatternMol) 414 415 TorsionMol, CalcStatus, Energy = MinimizeCalculateEnergyForTorsionMol(Mol, TorsionMol, TorsionAngle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, (MolIndex + 1)) 416 417 return (RDKitUtil.MolToBase64EncodedMolString(TorsionMol, PropertyPickleFlags = Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps), CalcStatus, TorsionAngle, Energy) 418 419 def GenerateBase64EncodedMolStringsWithTorsionScanInfo(Mol, MolIndex, TorsionMols, TorsionAngles, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, PropertyPickleFlags = Chem.PropertyPickleOptions.AllProps): 420 """Set up an iterator for generating base64 encoded molecule string for 421 a torsion in a molecule along with appropriate trosion scan information. 422 """ 423 424 for Index, TorsionMol in enumerate(TorsionMols): 425 yield [MolIndex, None, None, TorsionAngles[Index], TorsionID, TorsionPattern, None, TorsionMatches] if (Mol is None or TorsionMol is None) else [MolIndex, RDKitUtil.MolToBase64EncodedMolString(Mol, PropertyPickleFlags), RDKitUtil.MolToBase64EncodedMolString(TorsionMol, PropertyPickleFlags), TorsionAngles[Index], TorsionID, TorsionPattern, RDKitUtil.MolToBase64EncodedMolString(TorsionPatternMol, PropertyPickleFlags), TorsionMatches] 426 427 def PerformMinimizationAndTorsionScan(Mol, MolNum, UseMultiProcessingAtTorsionAnglesLevel = False): 428 """Perform minimization and torsions scan.""" 429 430 if not OptionsInfo["QuietMode"]: 431 MiscUtil.PrintInfo("\nProcessing molecule %s..." % (RDKitUtil.GetMolName(Mol, MolNum))) 432 433 Mol = AddHydrogens(Mol) 434 435 if not OptionsInfo["Infile3D"]: 436 Mol, MinimizationCalcStatus = MinimizeMolecule(Mol, MolNum) 437 if not MinimizationCalcStatus: 438 return (Mol, False, False, False) 439 440 TorsionsMolInfo = SetupTorsionsMolInfo(Mol, MolNum) 441 if TorsionsMolInfo["NumOfMatches"] == 0: 442 return (Mol, True, False, False) 443 444 Mol, ScanCalcStatus = ScanAllTorsionsInMol(Mol, TorsionsMolInfo, MolNum, UseMultiProcessingAtTorsionAnglesLevel) 445 if not ScanCalcStatus: 446 return (Mol, True, True, False) 447 448 return (Mol, True, True, True) 449 450 def ScanAllTorsionsInMol(Mol, TorsionsMolInfo, MolNum, UseMultiProcessingAtTorsionAnglesLevel = False): 451 """Perform scans on all torsions in a molecule.""" 452 453 if TorsionsMolInfo["NumOfMatches"] == 0: 454 return Mol, True 455 456 MolName = RDKitUtil.GetMolName(Mol, MolNum) 457 458 FirstTorsionMode = OptionsInfo["FirstTorsionMode"] 459 TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"] 460 461 TorsionPatternCount, TorsionScanCount, TorsionMatchCount = [0] * 3 462 TorsionMaxMatches = OptionsInfo["TorsionMaxMatches"] 463 464 for TorsionID in TorsionsPatternsInfo["IDs"]: 465 TorsionPatternCount += 1 466 TorsionPattern = TorsionsPatternsInfo["Pattern"][TorsionID] 467 TorsionPatternMol = TorsionsPatternsInfo["Mol"][TorsionID] 468 469 TorsionsMatches = TorsionsMolInfo["Matches"][TorsionID] 470 471 if TorsionsMatches is None: 472 continue 473 474 if FirstTorsionMode and TorsionPatternCount > 1: 475 if not OptionsInfo["QuietMode"]: 476 MiscUtil.PrintWarning("Already scaned first torsion pattern, \"%s\" for molecule %s during \"%s\" value of \"--modeTorsions\" option . Abandoning torsion scan...\n" % (TorsionPattern, MolName, OptionsInfo["ModeTorsions"])) 477 break 478 479 for Index, TorsionMatches in enumerate(TorsionsMatches): 480 TorsionMatchNum = Index + 1 481 TorsionMatchCount += 1 482 483 if TorsionMatchCount > TorsionMaxMatches: 484 if not OptionsInfo["QuietMode"]: 485 MiscUtil.PrintWarning("Already scaned a maximum of %s torsion matches for molecule %s specified by \"--torsionMaxMatches\" option. Abandoning torsion scan...\n" % (TorsionMaxMatches, MolName)) 486 break 487 488 TmpMol, TorsionScanStatus, TorsionMols, TorsionEnergies, TorsionAngles = ScanSingleTorsionInMol(Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, TorsionMatchNum, MolNum, UseMultiProcessingAtTorsionAnglesLevel) 489 if not TorsionScanStatus: 490 continue 491 492 TorsionScanCount += 1 493 GenerateOutputFiles(Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles) 494 495 if TorsionMatchCount > TorsionMaxMatches: 496 break 497 498 if OptionsInfo["RemoveHydrogens"]: 499 Mol = Chem.RemoveHs(Mol) 500 501 if TorsionScanCount: 502 GenerateStartingTorsionScanStructureOutfile(Mol, MolNum) 503 504 Status = True if TorsionScanCount else False 505 506 return (Mol, Status) 507 508 def ScanSingleTorsionInMol(Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, TorsionMatchNum, MolNum, UseMultiProcessingAtTorsionAnglesLevel): 509 """Perform torsion scan for a molecule along with constrained energy minimization.""" 510 511 if not OptionsInfo["QuietMode"]: 512 MiscUtil.PrintInfo("Processing torsion pattern, %s, match number, %s, in molecule %s..." % (TorsionPattern, TorsionMatchNum, RDKitUtil.GetMolName(Mol, MolNum))) 513 514 if UseMultiProcessingAtTorsionAnglesLevel: 515 return ScanSingleTorsionInMolUsingMultipleProcessesAtTorsionAnglesLevel(Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum) 516 else: 517 return ScanSingleTorsionInMolUsingSingleProcess(Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum) 518 519 def ScanSingleTorsionInMolUsingSingleProcess(Mol, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum): 520 """Perform torsion scan for a molecule using single processs along with constrained 521 energy minimization.""" 522 523 TorsionMols = [] 524 TorsionEnergies = [] 525 TorsionAngles = [] 526 527 Mols, Angles = SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum) 528 529 for Index, Angle in enumerate(Angles): 530 TorsionMol = Mols[Index] 531 TorsionMol, CalcStatus, Energy = MinimizeCalculateEnergyForTorsionMol(Mol, TorsionMol, Angle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum) 532 533 if not CalcStatus: 534 return (Mol, False, None, None, None) 535 536 if OptionsInfo["RemoveHydrogens"]: 537 TorsionMol = Chem.RemoveHs(TorsionMol) 538 539 TorsionMols.append(TorsionMol) 540 TorsionEnergies.append(Energy) 541 TorsionAngles.append(Angle) 542 543 return (Mol, True, TorsionMols, TorsionEnergies, TorsionAngles) 544 545 def SetupMolsForSingleTorsionScanInMol(Mol, TorsionMatches, MolNum = None): 546 """Setup molecules corresponding to all torsion angles in a molecule.""" 547 548 AtomIndex1, AtomIndex2, AtomIndex3, AtomIndex4 = TorsionMatches 549 550 TorsionMols = [] 551 TorsionAngles = OptionsInfo["TorsionAngles"] 552 553 for Angle in TorsionAngles: 554 TorsionMol = Chem.Mol(Mol) 555 TorsionMolConf = TorsionMol.GetConformer(0) 556 557 rdMolTransforms.SetDihedralDeg(TorsionMolConf, AtomIndex1, AtomIndex2, AtomIndex3, AtomIndex4, Angle) 558 TorsionMols.append(TorsionMol) 559 560 return (TorsionMols, TorsionAngles) 561 562 def MinimizeCalculateEnergyForTorsionMol(Mol, TorsionMol, TorsionAngle, TorsionID, TorsionPattern, TorsionPatternMol, TorsionMatches, MolNum): 563 """"Calculate energy of a torsion molecule by performing an optional constrained 564 energy minimzation. 565 """ 566 567 if OptionsInfo["TorsionMinimize"]: 568 # Perform constrained minimization... 569 TorsionMatchesMol = RDKitUtil.MolFromSubstructureMatch(TorsionMol, TorsionPatternMol, TorsionMatches) 570 TorsionMol, CalcStatus, Energy = ConstrainAndMinimizeMolecule(TorsionMol, TorsionMatchesMol, TorsionMatches, MolNum) 571 572 if not CalcStatus: 573 if not OptionsInfo["QuietMode"]: 574 MolName = RDKitUtil.GetMolName(Mol, MolNum) 575 MiscUtil.PrintWarning("Failed to perform constrained minimization for molecule %s with torsion angle set to %s during torsion scan for torsion pattern %s. Abandoning torsion scan..." % (MolName, TorsionAngle, TorsionPattern)) 576 return (TorsionMol, False, None) 577 else: 578 # Calculate energy... 579 CalcStatus, Energy = GetEnergy(TorsionMol) 580 if not CalcStatus: 581 if not OptionsInfo["QuietMode"]: 582 MolName = RDKitUtil.GetMolName(Mol, MolNum) 583 MiscUtil.PrintWarning("Failed to retrieve calculated energy for molecule %s with torsion angle set to %s during torsion scan for torsion pattern %s. Abandoning torsion scan..." % (MolName, TorsionAngle, TorsionPattern)) 584 return (TorsionMol, False, None) 585 586 return (TorsionMol, CalcStatus, Energy) 587 588 def SetupTorsionsMolInfo(Mol, MolNum = None): 589 """Setup torsions info for a molecule.""" 590 591 TorsionsPatternsInfo = OptionsInfo["TorsionsPatternsInfo"] 592 593 # Initialize... 594 TorsionsMolInfo = {} 595 TorsionsMolInfo["IDs"] = [] 596 TorsionsMolInfo["NumOfMatches"] = 0 597 TorsionsMolInfo["Matches"] = {} 598 for TorsionID in TorsionsPatternsInfo["IDs"]: 599 TorsionsMolInfo["IDs"].append(TorsionID) 600 TorsionsMolInfo["Matches"][TorsionID] = None 601 602 MolName = RDKitUtil.GetMolName(Mol, MolNum) 603 UseChirality = OptionsInfo["UseChirality"] 604 605 for TorsionID in TorsionsPatternsInfo["IDs"]: 606 # Match torsions.. 607 TorsionPattern = TorsionsPatternsInfo["Pattern"][TorsionID] 608 TorsionPatternMol = TorsionsPatternsInfo["Mol"][TorsionID] 609 TorsionsMatches = RDKitUtil.FilterSubstructureMatchesByAtomMapNumbers(Mol, TorsionPatternMol, Mol.GetSubstructMatches(TorsionPatternMol, useChirality = UseChirality)) 610 611 # Validate tosion matches... 612 ValidTorsionsMatches = [] 613 for Index, TorsionMatch in enumerate(TorsionsMatches): 614 if len(TorsionMatch) != 4: 615 if not OptionsInfo["QuietMode"]: 616 MiscUtil.PrintWarning("Ignoring invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: It must match exactly 4 atoms." % (TorsionMatch, TorsionPattern, MolName)) 617 continue 618 619 if not RDKitUtil.AreAtomIndicesSequentiallyConnected(Mol, TorsionMatch): 620 if not OptionsInfo["QuietMode"]: 621 MiscUtil.PrintInfo("") 622 MiscUtil.PrintWarning("Invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices must be sequentially connected." % (TorsionMatch, TorsionPattern, MolName)) 623 MiscUtil.PrintWarning("Reordering matched atom indices in a sequentially connected manner...") 624 625 Status, ReorderdTorsionMatch = RDKitUtil.ReorderAtomIndicesInSequentiallyConnectedManner(Mol, TorsionMatch) 626 if Status: 627 TorsionMatch = ReorderdTorsionMatch 628 if not OptionsInfo["QuietMode"]: 629 MiscUtil.PrintWarning("Successfully reordered torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices are now sequentially connected." % (TorsionMatch, TorsionPattern, MolName)) 630 else: 631 if not OptionsInfo["QuietMode"]: 632 MiscUtil.PrintWarning("Ignoring torsion match. Failed to reorder torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices are not sequentially connected." % (TorsionMatch, TorsionPattern, MolName)) 633 continue 634 635 Bond = Mol.GetBondBetweenAtoms(TorsionMatch[1], TorsionMatch[2]) 636 if Bond.IsInRing(): 637 if not OptionsInfo["QuietMode"]: 638 MiscUtil.PrintWarning("Ignoring invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices, %s and %s, are not allowed to be in a ring." % (TorsionMatch, TorsionPattern, MolName, TorsionMatch[1], TorsionMatch[2])) 639 continue 640 641 # Filter matched torsions... 642 if OptionsInfo["FilterTorsionsByAtomIndicesMode"]: 643 InvalidAtomIndices = [] 644 for AtomIndex in TorsionMatch: 645 if AtomIndex not in OptionsInfo["TorsionsFilterByAtomIndicesList"]: 646 InvalidAtomIndices.append(AtomIndex) 647 if len(InvalidAtomIndices): 648 if not OptionsInfo["QuietMode"]: 649 MiscUtil.PrintWarning("Ignoring invalid torsion match to atom indices, %s, for torsion pattern, %s, in molecule %s: Matched atom indices, %s, must be present in the list, %s, specified using option \"--torsionsFilterbyAtomIndices\"." % (TorsionMatch, TorsionPattern, MolName, InvalidAtomIndices, OptionsInfo["TorsionsFilterByAtomIndicesList"])) 650 continue 651 652 ValidTorsionsMatches.append(TorsionMatch) 653 654 # Track valid matches... 655 if len(ValidTorsionsMatches): 656 TorsionsMolInfo["NumOfMatches"] += len(ValidTorsionsMatches) 657 TorsionsMolInfo["Matches"][TorsionID] = ValidTorsionsMatches 658 659 if TorsionsMolInfo["NumOfMatches"] == 0: 660 if not OptionsInfo["QuietMode"]: 661 MiscUtil.PrintWarning("Failed to match any torsions in molecule %s" % (MolName)) 662 663 return TorsionsMolInfo 664 665 def SetupTorsionsPatternsInfo(): 666 """Setup torsions patterns info.""" 667 668 TorsionsPatternsInfo = {} 669 TorsionsPatternsInfo["IDs"] = [] 670 TorsionsPatternsInfo["Pattern"] = {} 671 TorsionsPatternsInfo["Mol"] = {} 672 673 TorsionID = 0 674 for TorsionPattern in OptionsInfo["TorsionPatternsList"]: 675 TorsionID += 1 676 677 TorsionMol = Chem.MolFromSmarts(TorsionPattern) 678 if TorsionMol is None: 679 MiscUtil.PrintError("Failed to create torsion pattern molecule. The torsion SMILES/SMARTS pattern, \"%s\", specified using \"-t, --torsions\" option is not valid." % (TorsionPattern)) 680 681 TorsionsPatternsInfo["IDs"].append(TorsionID) 682 TorsionsPatternsInfo["Pattern"][TorsionID] = TorsionPattern 683 TorsionsPatternsInfo["Mol"][TorsionID] = TorsionMol 684 685 OptionsInfo["TorsionsPatternsInfo"] = TorsionsPatternsInfo 686 687 def MinimizeMolecule(Mol, MolNum = None): 688 """Generate and minimize conformers for a molecule to get the lowest energy conformer.""" 689 690 if not OptionsInfo["QuietMode"]: 691 MiscUtil.PrintInfo("Minimizing molecule %s..." % (RDKitUtil.GetMolName(Mol, MolNum))) 692 693 ConfIDs = EmbedMolecule(Mol, MolNum) 694 if not len(ConfIDs): 695 if not OptionsInfo["QuietMode"]: 696 MolName = RDKitUtil.GetMolName(Mol, MolNum) 697 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s: Embedding failed...\n" % MolName) 698 return (Mol, False) 699 700 CalcEnergyMap = {} 701 for ConfID in ConfIDs: 702 try: 703 if OptionsInfo["UseUFF"]: 704 Status = AllChem.UFFOptimizeMolecule(Mol, confId = ConfID, maxIters = OptionsInfo["MaxIters"]) 705 elif OptionsInfo["UseMMFF"]: 706 Status = AllChem.MMFFOptimizeMolecule(Mol, confId = ConfID, maxIters = OptionsInfo["MaxIters"], mmffVariant = OptionsInfo["MMFFVariant"]) 707 else: 708 MiscUtil.PrintError("Minimization couldn't be performed: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"]) 709 except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg: 710 if not OptionsInfo["QuietMode"]: 711 MolName = RDKitUtil.GetMolName(Mol, MolNum) 712 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg)) 713 return (Mol, False) 714 715 EnergyStatus, Energy = GetEnergy(Mol, ConfID) 716 if not EnergyStatus: 717 if not OptionsInfo["QuietMode"]: 718 MolName = RDKitUtil.GetMolName(Mol, MolNum) 719 MiscUtil.PrintWarning("Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n" % (ConfID, MolName)) 720 return (Mol, False) 721 722 if Status != 0: 723 if not OptionsInfo["QuietMode"]: 724 MolName = RDKitUtil.GetMolName(Mol, MolNum) 725 MiscUtil.PrintWarning("Minimization failed to converge for conformation number %d of molecule %s in %d steps. Try using higher value for \"--maxIters\" option...\n" % (ConfID, MolName, OptionsInfo["MaxIters"])) 726 727 CalcEnergyMap[ConfID] = Energy 728 729 SortedConfIDs = sorted(ConfIDs, key = lambda ConfID: CalcEnergyMap[ConfID]) 730 MinEnergyConfID = SortedConfIDs[0] 731 732 for ConfID in [Conf.GetId() for Conf in Mol.GetConformers()]: 733 if ConfID == MinEnergyConfID: 734 continue 735 Mol.RemoveConformer(ConfID) 736 737 # Set ConfID to 0 for MinEnergyConf... 738 Mol.GetConformer(MinEnergyConfID).SetId(0) 739 740 return (Mol, True) 741 742 def ConstrainAndMinimizeMolecule(Mol, RefMolCore, RefMolMatches = None, MolNum = None): 743 """Constrain and Minimize molecule.""" 744 745 # Setup forcefield function to use for constrained minimization... 746 ForceFieldFunction = None 747 ForceFieldName = None 748 if OptionsInfo["UseUFF"]: 749 ForceFieldFunction = lambda mol, confId = -1 : AllChem.UFFGetMoleculeForceField(mol, confId = confId) 750 ForeceFieldName = "UFF" 751 else: 752 ForceFieldFunction = lambda mol, confId = -1 : AllChem.MMFFGetMoleculeForceField(mol, AllChem.MMFFGetMoleculeProperties(mol, mmffVariant = OptionsInfo["MMFFVariant"]) , confId = confId) 753 ForeceFieldName = "MMFF" 754 755 if ForceFieldFunction is None: 756 if not OptionsInfo["QuietMode"]: 757 MiscUtil.PrintWarning("Failed to setup forcefield %s for molecule: %s\n" % (ForceFieldName, RDKitUtil.GetMolName(Mol, MolNum))) 758 return (None, False, None) 759 760 MaxConfs = OptionsInfo["MaxConfsTorsion"] 761 EnforceChirality = OptionsInfo["EnforceChirality"] 762 UseExpTorsionAnglePrefs = OptionsInfo["UseExpTorsionAnglePrefs"] 763 ETVersion = OptionsInfo["ETVersion"] 764 UseBasicKnowledge = OptionsInfo["UseBasicKnowledge"] 765 UseTethers = OptionsInfo["UseTethers"] 766 767 CalcEnergyMap = {} 768 MolConfsMap = {} 769 ConfIDs = [ConfID for ConfID in range(0, MaxConfs)] 770 771 for ConfID in ConfIDs: 772 try: 773 MolConf = Chem.Mol(Mol) 774 RDKitUtil.ConstrainAndEmbed(MolConf, RefMolCore, coreMatchesMol = RefMolMatches, useTethers = UseTethers, coreConfId = -1, randomseed = ConfID, getForceField = ForceFieldFunction, enforceChirality = EnforceChirality, useExpTorsionAnglePrefs = UseExpTorsionAnglePrefs, useBasicKnowledge = UseBasicKnowledge, ETversion = ETVersion) 775 except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg: 776 if not OptionsInfo["QuietMode"]: 777 MolName = RDKitUtil.GetMolName(Mol, MolNum) 778 MiscUtil.PrintWarning("Constrained embedding couldn't be performed for molecule %s:\n%s\n" % (RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)) 779 return (None, False, None) 780 781 EnergyStatus, Energy = GetEnergy(MolConf) 782 783 if not EnergyStatus: 784 if not OptionsInfo["QuietMode"]: 785 MolName = RDKitUtil.GetMolName(Mol, MolNum) 786 MiscUtil.PrintWarning("Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n" % (ConfID, MolName)) 787 return (None, False, None) 788 789 CalcEnergyMap[ConfID] = Energy 790 MolConfsMap[ConfID] = MolConf 791 792 SortedConfIDs = sorted(ConfIDs, key = lambda ConfID: CalcEnergyMap[ConfID]) 793 MinEnergyConfID = SortedConfIDs[0] 794 795 MinEnergy = CalcEnergyMap[MinEnergyConfID] 796 MinEnergyMolConf = MolConfsMap[MinEnergyConfID] 797 798 MinEnergyMolConf.ClearProp('EmbedRMS') 799 800 return (MinEnergyMolConf, True, MinEnergy) 801 802 def GetEnergy(Mol, ConfID = None): 803 """Calculate energy.""" 804 805 Status = True 806 Energy = None 807 808 if ConfID is None: 809 ConfID = -1 810 811 if OptionsInfo["UseUFF"]: 812 UFFMoleculeForcefield = AllChem.UFFGetMoleculeForceField(Mol, confId = ConfID) 813 if UFFMoleculeForcefield is None: 814 Status = False 815 else: 816 Energy = UFFMoleculeForcefield.CalcEnergy() 817 elif OptionsInfo["UseMMFF"]: 818 MMFFMoleculeProperties = AllChem.MMFFGetMoleculeProperties(Mol, mmffVariant = OptionsInfo["MMFFVariant"]) 819 MMFFMoleculeForcefield = AllChem.MMFFGetMoleculeForceField(Mol, MMFFMoleculeProperties, confId = ConfID) 820 if MMFFMoleculeForcefield is None: 821 Status = False 822 else: 823 Energy = MMFFMoleculeForcefield.CalcEnergy() 824 else: 825 MiscUtil.PrintError("Couldn't retrieve conformer energy: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"]) 826 827 return (Status, Energy) 828 829 def EmbedMolecule(Mol, MolNum = None): 830 """Embed conformations.""" 831 832 ConfIDs = [] 833 834 MaxConfs = OptionsInfo["MaxConfs"] 835 RandomSeed = OptionsInfo["RandomSeed"] 836 EnforceChirality = OptionsInfo["EnforceChirality"] 837 UseExpTorsionAnglePrefs = OptionsInfo["UseExpTorsionAnglePrefs"] 838 ETVersion = OptionsInfo["ETVersion"] 839 UseBasicKnowledge = OptionsInfo["UseBasicKnowledge"] 840 841 try: 842 ConfIDs = AllChem.EmbedMultipleConfs(Mol, numConfs = MaxConfs, randomSeed = RandomSeed, enforceChirality = EnforceChirality, useExpTorsionAnglePrefs = UseExpTorsionAnglePrefs, useBasicKnowledge = UseBasicKnowledge, ETversion = ETVersion) 843 except ValueError as ErrMsg: 844 if not OptionsInfo["QuietMode"]: 845 MolName = RDKitUtil.GetMolName(Mol, MolNum) 846 MiscUtil.PrintWarning("Embedding failed for molecule %s:\n%s\n" % (MolName, ErrMsg)) 847 ConfIDs = [] 848 849 return ConfIDs 850 851 def GenerateStartingTorsionScanStructureOutfile(Mol, MolNum): 852 """Write out the structure of molecule used for starting tosion scan.""" 853 854 FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"]) 855 MolName = GetOutputFileMolName(Mol, MolNum) 856 857 Outfile = "%s_%s.%s" % (FileName, MolName, FileExt) 858 859 # Set up a molecule writer... 860 Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"]) 861 if Writer is None: 862 MiscUtil.PrintWarning("Failed to setup a writer for output fie %s " % Outfile) 863 return 864 865 Writer.write(Mol) 866 867 if Writer is not None: 868 Writer.close() 869 870 def GenerateOutputFiles(Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles): 871 """Generate output files.""" 872 873 StructureOutfile, EnergyTextOutfile, PlotOutfile = SetupOutputFileNames(Mol, MolNum, TorsionID, TorsionMatchNum) 874 875 GenerateScannedTorsionsStructureOutfile(StructureOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles) 876 GenerateEnergyTextOutfile(EnergyTextOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles) 877 GeneratePlotOutfile(PlotOutfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles) 878 879 def GenerateScannedTorsionsStructureOutfile(Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles): 880 """Write out structures generated after torsion scan along with associated data.""" 881 882 # Set up a molecule writer... 883 Writer = RDKitUtil.MoleculesWriter(Outfile, **OptionsInfo["OutfileParams"]) 884 if Writer is None: 885 MiscUtil.PrintWarning("Failed to setup a writer for output fie %s " % Outfile) 886 return 887 888 MolName = RDKitUtil.GetMolName(Mol, MolNum) 889 890 RelativeTorsionEnergies = SetupRelativeEnergies(TorsionEnergies) 891 for Index, TorsionMol in enumerate(TorsionMols): 892 TorsionAngle = "%s" % TorsionAngles[Index] 893 TorsionMol.SetProp("Torsion_Angle", TorsionAngle) 894 895 TorsionEnergy = "%.2f" % TorsionEnergies[Index] 896 TorsionMol.SetProp(OptionsInfo["EnergyLabel"], TorsionEnergy) 897 898 RelativeTorsionEnergy = "%.2f" % RelativeTorsionEnergies[Index] 899 TorsionMol.SetProp(OptionsInfo["RelativeEnergyLabel"], RelativeTorsionEnergy) 900 901 TorsionMolName = "%s_Deg%s" % (MolName, TorsionAngle) 902 TorsionMol.SetProp("_Name", TorsionMolName) 903 904 Writer.write(TorsionMol) 905 906 if Writer is not None: 907 Writer.close() 908 909 def GenerateEnergyTextOutfile(Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles): 910 """Write out torsion angles and energies.""" 911 912 # Setup a writer... 913 Writer = open(Outfile, "w") 914 if Writer is None: 915 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % Outfile) 916 917 # Write headers... 918 Writer.write("TorsionAngle,%s,%s\n" % (OptionsInfo["EnergyLabel"], OptionsInfo["RelativeEnergyLabel"])) 919 920 RelativeTorsionEnergies = SetupRelativeEnergies(TorsionEnergies) 921 for Index, TorsionAngle in enumerate(TorsionAngles): 922 Writer.write("%d,%.2f,%.2f\n" % (TorsionAngle, TorsionEnergies[Index], RelativeTorsionEnergies[Index])) 923 924 if Writer is not None: 925 Writer.close() 926 927 def GeneratePlotOutfile(Outfile, Mol, MolNum, TorsionID, TorsionMatchNum, TorsionMols, TorsionEnergies, TorsionAngles): 928 """Generate a plot corresponding to torsion angles and energies.""" 929 930 OutPlotParams = OptionsInfo["OutPlotParams"] 931 932 # Initialize seaborn and matplotlib paramaters... 933 if not OptionsInfo["OutPlotInitialized"]: 934 OptionsInfo["OutPlotInitialized"] = True 935 RCParams = {"figure.figsize":(OutPlotParams["Width"], OutPlotParams["Height"]), 936 "axes.titleweight": OutPlotParams["TitleWeight"], 937 "axes.labelweight": OutPlotParams["LabelWeight"]} 938 sns.set(context = OutPlotParams["Context"], style = OutPlotParams["Style"], palette = OutPlotParams["Palette"], font = OutPlotParams["Font"], font_scale = OutPlotParams["FontScale"], rc = RCParams) 939 940 # Create a new figure... 941 plt.figure() 942 943 if OptionsInfo["OutPlotRelativeEnergy"]: 944 TorsionEnergies = SetupRelativeEnergies(TorsionEnergies) 945 946 # Draw plot... 947 PlotType = OutPlotParams["Type"] 948 if re.match("linepoint", PlotType, re.I): 949 Axis = sns.lineplot(x = TorsionAngles, y = TorsionEnergies, marker = "o", legend = False) 950 elif re.match("scatter", PlotType, re.I): 951 Axis = sns.scatterplot(x = TorsionAngles, y = TorsionEnergies, legend = False) 952 elif re.match("line", PlotType, re.I): 953 Axis = sns.lineplot(x = TorsionAngles, y = TorsionEnergies, legend = False) 954 else: 955 MiscUtil.PrintError("The value, %s, specified for \"type\" using option \"--outPlotParams\" is not supported. Valid plot types: linepoint, scatter or line" % (PlotType)) 956 957 # Setup title and labels... 958 Title = OutPlotParams["Title"] 959 if OptionsInfo["OutPlotTitleTorsionSpec"]: 960 TorsionPattern = OptionsInfo["TorsionsPatternsInfo"]["Pattern"][TorsionID] 961 Title = "%s: %s" % (OutPlotParams["Title"], TorsionPattern) 962 963 # Set labels and title... 964 Axis.set(xlabel = OutPlotParams["XLabel"], ylabel = OutPlotParams["YLabel"], title = Title) 965 966 # Save figure... 967 plt.savefig(Outfile) 968 969 # Close the plot... 970 plt.close() 971 972 def SetupRelativeEnergies(Energies): 973 """Set up a list of relative energies.""" 974 975 SortedEnergies = sorted(Energies) 976 MinEnergy = SortedEnergies[0] 977 RelativeEnergies = [(Energy - MinEnergy) for Energy in Energies] 978 979 return RelativeEnergies 980 981 def SetupOutputFileNames(Mol, MolNum, TorsionID, TorsionMatchNum): 982 """Setup names of output files.""" 983 984 FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"]) 985 MolName = GetOutputFileMolName(Mol, MolNum) 986 987 OutfileRoot = "%s_%s_Torsion%s_Match%s" % (FileName, MolName, TorsionID, TorsionMatchNum) 988 989 StructureOutfile = "%s.%s" % (OutfileRoot, FileExt) 990 EnergyTextOutfile = "%s_Energies.csv" % (OutfileRoot) 991 PlotExt = OptionsInfo["OutPlotParams"]["OutExt"] 992 PlotOutfile = "%s_Plot.%s" % (OutfileRoot, PlotExt) 993 994 return (StructureOutfile, EnergyTextOutfile, PlotOutfile) 995 996 def GetOutputFileMolName(Mol, MolNum): 997 """Get output file prefix.""" 998 999 MolName = "Mol%s" % MolNum 1000 if OptionsInfo["OutfileMolName"]: 1001 MolName = re.sub("[^a-zA-Z0-9]", "_", RDKitUtil.GetMolName(Mol, MolNum), re.I) 1002 1003 return MolName 1004 1005 def AddHydrogens(Mol, AddCoords = True): 1006 """Check and add hydrogens.""" 1007 1008 if not OptionsInfo["AddHydrogens"]: 1009 return Mol 1010 1011 return Chem.AddHs(Mol, addCoords = AddCoords) 1012 1013 def ProcessTorsionRangeOptions(): 1014 """Process tosion range options. """ 1015 1016 TosionRangeMode = Options["--torsionRangeMode"] 1017 OptionsInfo["TosionRangeMode"] = TosionRangeMode 1018 1019 if re.match("^Range$", TosionRangeMode, re.I): 1020 ProcessTorsionRangeValues() 1021 elif re.match("^Angles$", TosionRangeMode, re.I): 1022 ProcessTorsionAnglesValues() 1023 else: 1024 MiscUtil.PrintError("The value, %s, option \"--torsionRangeMode\" is not supported." % TosionRangeMode) 1025 1026 def ProcessTorsionRangeValues(): 1027 """Process tosion range values. """ 1028 1029 TorsionRange = Options["--torsionRange"] 1030 if re.match("^auto$", TorsionRange, re.I): 1031 TorsionRange = "0,360,5" 1032 TorsionRangeWords = TorsionRange.split(",") 1033 1034 TorsionStart = int(TorsionRangeWords[0]) 1035 TorsionStop = int(TorsionRangeWords[1]) 1036 TorsionStep = int(TorsionRangeWords[2]) 1037 1038 if TorsionStart >= TorsionStop: 1039 MiscUtil.PrintError("The start value, %d, specified for option \"--torsionRange\" in string \"%s\" must be less than stop value, %s." % (TorsionStart, Options["--torsionRange"], TorsionStop)) 1040 if TorsionStep == 0: 1041 MiscUtil.PrintError("The step value, %d, specified for option \"--torsonRange\" in string \"%s\" must be > 0." % (TorsionStep, Options["--torsionRange"])) 1042 if TorsionStep >= (TorsionStop - TorsionStart): 1043 MiscUtil.PrintError("The step value, %d, specified for option \"--torsonRange\" in string \"%s\" must be less than, %s." % (TorsionStep, Options["--torsionRange"], (TorsionStop - TorsionStart))) 1044 1045 if TorsionStart < 0: 1046 if TorsionStart < -180: 1047 MiscUtil.PrintError("The start value, %d, specified for option \"--torsionRange\" in string \"%s\" must be >= -180 to use scan range from -180 to 180." % (TorsionStart, Options["--torsionRange"])) 1048 if TorsionStop > 180: 1049 MiscUtil.PrintError("The stop value, %d, specified for option \"--torsionRange\" in string \"%s\" must be <= 180 to use scan range from -180 to 180." % (TorsionStop, Options["--torsionRange"])) 1050 else: 1051 if TorsionStop > 360: 1052 MiscUtil.PrintError("The stop value, %d, specified for option \"--torsionRange\" in string \"%s\" must be <= 360 to use scan range from 0 to 360." % (TorsionStop, Options["--torsionRange"])) 1053 1054 TorsionAngles = [Angle for Angle in range(TorsionStart, TorsionStop, TorsionStep)] 1055 TorsionAngles.append(TorsionStop) 1056 1057 OptionsInfo["TorsionRange"] = TorsionRange 1058 OptionsInfo["TorsionStart"] = TorsionStart 1059 OptionsInfo["TorsionStop"] = TorsionStop 1060 OptionsInfo["TorsionStep"] = TorsionStep 1061 1062 OptionsInfo["TorsionAngles"] = TorsionAngles 1063 1064 def ProcessTorsionAnglesValues(): 1065 """Process tosion angle values. """ 1066 1067 TorsionRange = Options["--torsionRange"] 1068 if re.match("^auto$", TorsionRange, re.I): 1069 MiscUtil.PrintError("The value specified, %s, for option \"--torsionRange\" is not valid." % (TorsionRange)) 1070 1071 TorsionAngles = [] 1072 1073 for TorsionAngle in TorsionRange.split(","): 1074 TorsionAngle = int(TorsionAngle) 1075 1076 if TorsionAngle < -180: 1077 MiscUtil.PrintError("The stop value, %d, specified for option \"--torsionRange\" in string \"%s\" must be >= -180." % (TorsionAngle, TorsionRange)) 1078 1079 if TorsionAngle > 360: 1080 MiscUtil.PrintError("The stop value, %d, specified for option \"--torsionRange\" in string \"%s\" must be <= 360." % (TorsionAngle, TorsionRange)) 1081 1082 TorsionAngles.append(TorsionAngle) 1083 1084 OptionsInfo["TorsionRange"] = TorsionRange 1085 OptionsInfo["TorsionStart"] = None 1086 OptionsInfo["TorsionStop"] = None 1087 OptionsInfo["TorsionStep"] = None 1088 1089 OptionsInfo["TorsionAngles"] = sorted(TorsionAngles) 1090 1091 def ProcesssConformerGeneratorOption(): 1092 """Process comformer generator option. """ 1093 1094 ConfGenParams = MiscUtil.ProcessOptionConformerGenerator("--conformerGenerator", Options["--conformerGenerator"]) 1095 1096 OptionsInfo["ConformerGenerator"] = ConfGenParams["ConformerGenerator"] 1097 OptionsInfo["UseBasicKnowledge"] = ConfGenParams["UseBasicKnowledge"] 1098 OptionsInfo["UseExpTorsionAnglePrefs"] = ConfGenParams["UseExpTorsionAnglePrefs"] 1099 OptionsInfo["ETVersion"] = ConfGenParams["ETVersion"] 1100 1101 def ProcessOptions(): 1102 """Process and validate command line arguments and options.""" 1103 1104 MiscUtil.PrintInfo("Processing options...") 1105 1106 # Validate options... 1107 ValidateOptions() 1108 1109 OptionsInfo["ModeMols"] = Options["--modeMols"] 1110 OptionsInfo["FirstMolMode"] = True if re.match("^First$", Options["--modeMols"], re.I) else False 1111 1112 OptionsInfo["ModeTorsions"] = Options["--modeTorsions"] 1113 OptionsInfo["FirstTorsionMode"] = True if re.match("^First$", Options["--modeTorsions"], re.I) else False 1114 1115 OptionsInfo["Infile"] = Options["--infile"] 1116 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters("--infileParams", Options["--infileParams"], Options["--infile"]) 1117 OptionsInfo["Infile3D"] = True if re.match("^yes$", Options["--infile3D"], re.I) else False 1118 1119 OptionsInfo["Outfile"] = Options["--outfile"] 1120 OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters("--outfileParams", Options["--outfileParams"]) 1121 1122 OptionsInfo["OutfileMolName"] = True if re.match("^yes$", Options["--outfileMolName"], re.I) else False 1123 1124 OptionsInfo["OutPlotRelativeEnergy"] = True if re.match("^yes$", Options["--outPlotRelativeEnergy"], re.I) else False 1125 OptionsInfo["OutPlotTitleTorsionSpec"] = True if re.match("^yes$", Options["--outPlotTitleTorsionSpec"], re.I) else False 1126 1127 # The default width and height, 10.0 and 7.5, map to aspect raito of 16/9 (1.778)... 1128 DefaultValues = {'Type': 'linepoint', 'Width': 10.0, 'Height': 5.6, 'Title': 'RDKit Torsion Scan', 'XLabel': 'Torsion Angle (degrees)', 'YLabel': 'Energy (kcal/mol)'} 1129 if OptionsInfo["OutPlotRelativeEnergy"]: 1130 DefaultValues["YLabel"] = "Relative Energy (kcal/mol)" 1131 OptionsInfo["OutPlotParams"] = MiscUtil.ProcessOptionSeabornPlotParameters("--outPlotParams", Options["--outPlotParams"], DefaultValues) 1132 if not re.match("^(linepoint|scatter|Line)$", OptionsInfo["OutPlotParams"]["Type"], re.I): 1133 MiscUtil.PrintError("The value, %s, specified for \"type\" using option \"--outPlotParams\" is not supported. Valid plot types: linepoint, scatter or line" % (OptionsInfo["OutPlotParams"]["Type"])) 1134 1135 OptionsInfo["OutPlotInitialized"] = False 1136 1137 # Procsss and validate specified SMILES/SMARTS torsion patterns... 1138 TorsionPatterns = Options["--torsions"] 1139 TorsionPatternsList = [] 1140 for TorsionPattern in TorsionPatterns.split(","): 1141 TorsionPattern = TorsionPattern.strip() 1142 if not len(TorsionPattern): 1143 MiscUtil.PrintError("Empty value specified for SMILES/SMARTS pattern in \"-t, --torsions\" option: %s" % TorsionPatterns) 1144 1145 TorsionMol = Chem.MolFromSmarts(TorsionPattern) 1146 if TorsionMol is None: 1147 MiscUtil.PrintError("Failed to create torsion pattern molecule. The torsion SMILES/SMARTS pattern, \"%s\", specified using \"-t, --torsions\" option, \"%s\", is not valid." % (TorsionPattern, TorsionPatterns)) 1148 TorsionPatternsList.append(TorsionPattern) 1149 1150 OptionsInfo["TorsionPatterns"] = TorsionPatterns 1151 OptionsInfo["TorsionPatternsList"] = TorsionPatternsList 1152 1153 # Process and validate any specified torsion atom indices for filtering torsion matches... 1154 TorsionsFilterByAtomIndices = Options["--torsionsFilterbyAtomIndices"] 1155 TorsionsFilterByAtomIndicesList = [] 1156 if not re.match("^None$", TorsionsFilterByAtomIndices, re.I): 1157 for AtomIndex in TorsionsFilterByAtomIndices.split(","): 1158 AtomIndex = AtomIndex.strip() 1159 if not MiscUtil.IsInteger(AtomIndex): 1160 MiscUtil.PrintError("The value specified, %s, for option \"--torsionsFilterbyAtomIndices\" must be an integer." % AtomIndex) 1161 AtomIndex = int(AtomIndex) 1162 if AtomIndex < 0: 1163 MiscUtil.PrintError("The value specified, %s, for option \"--torsionsFilterbyAtomIndices\" must be >= 0." % AtomIdex) 1164 TorsionsFilterByAtomIndicesList.append(AtomIndex) 1165 1166 if len(TorsionsFilterByAtomIndicesList) < 4: 1167 MiscUtil.PrintError("The number of values, %s, specified, %s, for option \"--torsionsFilterbyAtomIndices\" must be >=4." % (len(TorsionsFilterByAtomIndicesList), TorsionsFilterByAtomIndices)) 1168 1169 OptionsInfo["TorsionsFilterByAtomIndices"] = TorsionsFilterByAtomIndices 1170 OptionsInfo["TorsionsFilterByAtomIndicesList"] = TorsionsFilterByAtomIndicesList 1171 OptionsInfo["FilterTorsionsByAtomIndicesMode"] = True if len(TorsionsFilterByAtomIndicesList) > 0 else False 1172 1173 OptionsInfo["Overwrite"] = Options["--overwrite"] 1174 1175 OptionsInfo["AddHydrogens"] = True if re.match("^yes$", Options["--addHydrogens"], re.I) else False 1176 1177 ProcesssConformerGeneratorOption() 1178 1179 if re.match("^UFF$", Options["--forceField"], re.I): 1180 ForceField = "UFF" 1181 UseUFF = True 1182 UseMMFF = False 1183 elif re.match("^MMFF$", Options["--forceField"], re.I): 1184 ForceField = "MMFF" 1185 UseUFF = False 1186 UseMMFF = True 1187 else: 1188 MiscUtil.PrintError("The value, %s, specified for \"--forceField\" is not supported." % (Options["--forceField"],)) 1189 1190 MMFFVariant = "MMFF94" if re.match("^MMFF94$", Options["--forceFieldMMFFVariant"], re.I) else "MMFF94s" 1191 1192 OptionsInfo["ForceField"] = ForceField 1193 OptionsInfo["MMFFVariant"] = MMFFVariant 1194 OptionsInfo["UseMMFF"] = UseMMFF 1195 OptionsInfo["UseUFF"] = UseUFF 1196 1197 EnergyUnits = "kcal/mol" 1198 if UseMMFF: 1199 OptionsInfo["EnergyLabel"] = "%s_Energy" % MMFFVariant 1200 OptionsInfo["RelativeEnergyLabel"] = "%s_Relative_Energy" % MMFFVariant 1201 else: 1202 OptionsInfo["EnergyLabel"] = "%s_Energy" % ForceField 1203 OptionsInfo["RelativeEnergyLabel"] = "%s_Relative_Energy" % ForceField 1204 1205 OptionsInfo["EnforceChirality"] = True if re.match("^yes$", Options["--enforceChirality"], re.I) else False 1206 1207 OptionsInfo["MaxConfs"] = int(Options["--maxConfs"]) 1208 OptionsInfo["MaxConfsTorsion"] = int(Options["--maxConfsTorsion"]) 1209 OptionsInfo["MaxIters"] = int(Options["--maxIters"]) 1210 1211 OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False 1212 OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"]) 1213 1214 # Multiprocessing level... 1215 MPLevelMoleculesMode = False 1216 MPLevelTorsionAnglesMode = False 1217 MPLevel = Options["--mpLevel"] 1218 if re.match("^Molecules$", MPLevel, re.I): 1219 MPLevelMoleculesMode = True 1220 elif re.match("^TorsionAngles$", MPLevel, re.I): 1221 MPLevelTorsionAnglesMode = True 1222 else: 1223 MiscUtil.PrintError("The value, %s, specified for option \"--mpLevel\" is not valid. " % MPLevel) 1224 OptionsInfo["MPLevel"] = MPLevel 1225 OptionsInfo["MPLevelMoleculesMode"] = MPLevelMoleculesMode 1226 OptionsInfo["MPLevelTorsionAnglesMode"] = MPLevelTorsionAnglesMode 1227 1228 OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False 1229 1230 RandomSeed = -1 1231 if not re.match("^auto$", Options["--randomSeed"], re.I): 1232 RandomSeed = int(Options["--randomSeed"]) 1233 OptionsInfo["RandomSeed"] = RandomSeed 1234 1235 OptionsInfo["RemoveHydrogens"] = True if re.match("^yes$", Options["--removeHydrogens"], re.I) else False 1236 1237 OptionsInfo["TorsionMaxMatches"] = int(Options["--torsionMaxMatches"]) 1238 OptionsInfo["TorsionMinimize"] = True if re.match("^yes$", Options["--torsionMinimize"], re.I) else False 1239 1240 ProcessTorsionRangeOptions() 1241 1242 OptionsInfo["UseTethers"] = True if re.match("^yes$", Options["--useTethers"], re.I) else False 1243 OptionsInfo["UseChirality"] = True if re.match("^yes$", Options["--useChirality"], re.I) else False 1244 1245 def RetrieveOptions(): 1246 """Retrieve command line arguments and options.""" 1247 1248 # Get options... 1249 global Options 1250 Options = docopt(_docoptUsage_) 1251 1252 # Set current working directory to the specified directory... 1253 WorkingDir = Options["--workingdir"] 1254 if WorkingDir: 1255 os.chdir(WorkingDir) 1256 1257 # Handle examples option... 1258 if "--examples" in Options and Options["--examples"]: 1259 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_)) 1260 sys.exit(0) 1261 1262 def ValidateOptions(): 1263 """Validate option values.""" 1264 1265 MiscUtil.ValidateOptionTextValue("-a, --addHydrogens", Options["--addHydrogens"], "yes no") 1266 MiscUtil.ValidateOptionTextValue("-c, --conformerGenerator", Options["--conformerGenerator"], "SDG KDG ETDG ETKDG ETKDGv2") 1267 1268 MiscUtil.ValidateOptionTextValue("-f, --forceField", Options["--forceField"], "UFF MMFF") 1269 MiscUtil.ValidateOptionTextValue(" --forceFieldMMFFVariant", Options["--forceFieldMMFFVariant"], "MMFF94 MMFF94s") 1270 1271 MiscUtil.ValidateOptionTextValue("--enforceChirality ", Options["--enforceChirality"], "yes no") 1272 1273 MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"]) 1274 MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv") 1275 MiscUtil.ValidateOptionTextValue("--infile3D", Options["--infile3D"], "yes no") 1276 1277 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd") 1278 MiscUtil.ValidateOptionsOutputFileOverwrite("-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]) 1279 MiscUtil.ValidateOptionsDistinctFileNames("-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]) 1280 1281 if not Options["--overwrite"]: 1282 FileDir, FileName, FileExt = MiscUtil.ParseFileName(Options["--outfile"]) 1283 FileNames = glob.glob("%s_*" % FileName) 1284 if len(FileNames): 1285 MiscUtil.PrintError("The outfile names, %s_*, generated from file specified, %s, for option \"-o, --outfile\" already exist. Use option \"--overwrite\" or \"--ov\" and try again.\n" % (FileName, Options["--outfile"])) 1286 1287 MiscUtil.ValidateOptionTextValue("--outPlotRelativeEnergy", Options["--outPlotRelativeEnergy"], "yes no") 1288 MiscUtil.ValidateOptionTextValue("--outPlotTitleTorsionSpec", Options["--outPlotTitleTorsionSpec"], "yes no") 1289 1290 MiscUtil.ValidateOptionTextValue("--outfileMolName ", Options["--outfileMolName"], "yes no") 1291 1292 MiscUtil.ValidateOptionTextValue("--modeMols", Options["--modeMols"], "First All") 1293 MiscUtil.ValidateOptionTextValue("--modeTorsions", Options["--modeTorsions"], "First All") 1294 1295 MiscUtil.ValidateOptionIntegerValue("--maxConfs", Options["--maxConfs"], {">": 0}) 1296 MiscUtil.ValidateOptionIntegerValue("--maxConfsTorsion", Options["--maxConfsTorsion"], {">": 0}) 1297 MiscUtil.ValidateOptionIntegerValue("--maxIters", Options["--maxIters"], {">": 0}) 1298 1299 MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no") 1300 MiscUtil.ValidateOptionTextValue("--mpLevel", Options["--mpLevel"], "Molecules TorsionAngles") 1301 MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no") 1302 1303 if not re.match("^auto$", Options["--randomSeed"], re.I): 1304 MiscUtil.ValidateOptionIntegerValue("--randomSeed", Options["--randomSeed"], {}) 1305 1306 MiscUtil.ValidateOptionTextValue("-r, --removeHydrogens", Options["--removeHydrogens"], "yes no") 1307 1308 MiscUtil.ValidateOptionIntegerValue("--torsionMaxMatches", Options["--torsionMaxMatches"], {">": 0}) 1309 MiscUtil.ValidateOptionTextValue("--torsionMinimize", Options["--torsionMinimize"], "yes no") 1310 1311 MiscUtil.ValidateOptionTextValue("--torsionRangeMode", Options["--torsionRangeMode"], "Range or Angles") 1312 TorsionRange = Options["--torsionRange"] 1313 if re.match("^Range$", Options["--torsionRangeMode"], re.I): 1314 if not re.match("^auto$", TorsionRange, re.I): 1315 MiscUtil.ValidateOptionNumberValues("--torsionRange", TorsionRange, 3, ",", "integer", {}) 1316 else: 1317 if re.match("^auto$", TorsionRange, re.I): 1318 MiscUtil.PrintError("The value, %s, specified for option \"-torsionRange\" is not valid for \"%s\" value of \"--torsionRangeMode\" option. You must specify a torsion angle or a comma delimited list of torsion angles." % (TorsionRange, Options["--torsionRangeMode"])) 1319 TorsionAngles = [] 1320 for TorsionAngle in TorsionRange.split(","): 1321 TorsionAngle = TorsionAngle.strip() 1322 if not MiscUtil.IsInteger(TorsionAngle): 1323 MiscUtil.PrintError("The value specified, %s, for option \"--torsionRange\" in string \"%s\" must be an integer." % (TorsionAngle, TorsionRange)) 1324 if TorsionAngle in TorsionAngles: 1325 MiscUtil.PrintError("The value specified, %s, for option \"--torsionRange\" in string \"%s\" is a duplicate value." % (TorsionAngle, TorsionRange)) 1326 TorsionAngles.append(TorsionAngle) 1327 1328 MiscUtil.ValidateOptionTextValue("--useChirality", Options["--useChirality"], "yes no") 1329 MiscUtil.ValidateOptionTextValue("--useTethers", Options["--useTethers"], "yes no") 1330 1331 # Setup a usage string for docopt... 1332 _docoptUsage_ = """ 1333 RDKitPerformTorsionScan.py - Perform torsion scan 1334 1335 Usage: 1336 RDKitPerformTorsionScan.py [--addHydrogens <yes or no>] [--conformerGenerator <SDG, KDG, ETDG, ETKDG, ETKDGv2>] 1337 [--forceField <UFF, or MMFF>] [--forceFieldMMFFVariant <MMFF94 or MMFF94s>] 1338 [--enforceChirality <yes or no>] [--infile3D <yes or no>] [--infileParams <Name,Value,...>] 1339 [--modeMols <First or All>] [--modeTorsions <First or All> ] [--maxConfs <number>] 1340 [--maxConfsTorsion <number>] [--maxIters <number>] [--mp <yes or no>] 1341 [--mpLevel <Molecules or TorsionAngles>] [--mpParams <Name,Value,...>] 1342 [--outfileMolName <yes or no>] [--outfileParams <Name,Value,...>] [--outPlotParams <Name,Value,...>] 1343 [--outPlotRelativeEnergy <yes or no>] [--outPlotTitleTorsionSpec <yes or no>] [--overwrite] [--quiet <yes or no>] 1344 [--removeHydrogens <yes or no>] [--randomSeed <number>] [--torsionsFilterbyAtomIndices <Index1, Index2, ...>] 1345 [--torsionMaxMatches <number>] [--torsionMinimize <yes or no>] [--torsionRangeMode <Range or Angles>] 1346 [--torsionRange <Start,Stop,Step or Angle1,Angle2,...>] [--useChirality <yes or no>] [--useTethers <yes or no>] 1347 [-w <dir>] -t <torsions> -i <infile> -o <outfile> 1348 RDKitPerformTorsionScan.py -h | --help | -e | --examples 1349 1350 Description: 1351 Perform torsion scan for molecules around torsion angles specified using 1352 SMILES/SMARTS patterns. A molecule is optionally minimized before performing 1353 a torsion scan. A set of initial 3D structures are generated for a molecule 1354 by scanning the torsion angle across the specified range and updating the 3D 1355 coordinates of the molecule. A conformation ensemble is optionally generated 1356 for each 3D structure representing a specific torsion angle. The conformation 1357 with the lowest energy is selected to represent the torsion angle. An option 1358 is available to skip the generation of the conformation ensemble and simply 1359 calculate the energy for the initial 3D structure for a specific torsion angle. 1360 1361 The torsions are specified using SMILES or SMARTS patterns. A substructure match 1362 is performed to select torsion atoms in a molecule. The SMILES pattern match must 1363 correspond to four torsion atoms. The SMARTS patterns containing atom map numbers 1364 may match more than four atoms. The atom map numbers, however, must match 1365 exactly four torsion atoms. For example: [s:1][c:2]([aX2,cH1])!@[CX3:3](O)=[O:4] for 1366 thiophene esters and carboxylates as specified in Torsion Library (TorLib) [Ref 146]. 1367 1368 A set of four output files is generated for each torsion match in each 1369 molecule. The names of the output files are generated using the root of 1370 the specified output file. They may either contain sequential molecule 1371 numbers or molecule names as shown below: 1372 1373 <OutfileRoot>_Mol<Num>.sdf 1374 <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>.sdf 1375 <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>_Energies.csv 1376 <OutfileRoot>_Mol<Num>_Torsion<Num>_Match<Num>_Plot.<ImgExt> 1377 1378 or 1379 1380 <OutfileRoot>_<MolName>.sdf 1381 <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>.sdf 1382 <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>_Energies.csv 1383 <OutfileRoot>_<MolName>_Torsion<Num>_Match<Num>_Plot.<ImgExt> 1384 1385 The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi, 1386 .csv, .tsv, .txt) 1387 1388 The supported output file formats are: SD (.sdf, .sd) 1389 1390 Options: 1391 -a, --addHydrogens <yes or no> [default: yes] 1392 Add hydrogens before minimization. 1393 -c, --conformerGenerator <text> [default: ETKDGv2] 1394 Conformation generation methodology for generating initial 3D structure 1395 of a molecule and conformation ensemble representing a specific torsion 1396 angle. No conformation ensemble is generated for 'No' value of 1397 '--torsionMinimize' option. 1398 1399 The possible values along with a brief description are shown below: 1400 1401 SDG: Standard Distance Geometry 1402 KDG: basic Knowledge-terms with Distance Geometry 1403 ETDG: Experimental Torsion-angle preference with Distance Geometry 1404 ETKDG: Experimental Torsion-angle preference along with basic 1405 Knowledge-terms and Distance Geometry [Ref 129] 1406 ETKDGv2: Experimental Torsion-angle preference along with basic 1407 Knowledge-terms and Distance Geometry [Ref 167] 1408 -f, --forceField <UFF, MMFF> [default: MMFF] 1409 Forcefield method to use for energy minimization of initial 3D structure 1410 of a molecule and conformation ensemble representing a specific torsion. 1411 No conformation ensemble is generated during for 'No' value of '--torsionMinimze' 1412 option and constrained energy minimization is not performed. Possible values: 1413 Universal Force Field (UFF) [ Ref 81 ] or Merck Molecular Mechanics Force 1414 Field [ Ref 83-87 ] . 1415 --forceFieldMMFFVariant <MMFF94 or MMFF94s> [default: MMFF94] 1416 Variant of MMFF forcefield to use for energy minimization. 1417 --enforceChirality <yes or no> [default: Yes] 1418 Enforce chirality for defined chiral centers during generation of conformers. 1419 -e, --examples 1420 Print examples. 1421 -h, --help 1422 Print this help message. 1423 -i, --infile <infile> 1424 Input file name. 1425 --infile3D <yes or no> [default: no] 1426 Skip generation and minimization of initial 3D structures for molecules in 1427 input file containing 3D coordinates. 1428 --infileParams <Name,Value,...> [default: auto] 1429 A comma delimited list of parameter name and value pairs for reading 1430 molecules from files. The supported parameter names for different file 1431 formats, along with their default values, are shown below: 1432 1433 SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes 1434 1435 SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space, 1436 smilesTitleLine,auto,sanitize,yes 1437 1438 Possible values for smilesDelimiter: space, comma or tab. 1439 --modeMols <First or All> [default: First] 1440 Perform torsion scan for the first molecule or all molecules in input 1441 file. 1442 --modeTorsions <First or All> [default: First] 1443 Perform torsion scan for the first or all specified torsion pattern in 1444 molecules up to a maximum number of matches for each torsion 1445 specification as indicated by '--torsionMaxMatches' option. 1446 --maxConfs <number> [default: 250] 1447 Maximum number of conformations to generate for initial 3D structure of a 1448 molecule. The lowest energy conformation is written to the output file. 1449 --maxConfsTorsion <number> [default: 50] 1450 Maximum number of conformations to generate for conformation ensemble 1451 representing a specific torsion. A constrained minimization is performed 1452 using the coordinates of the specified torsion and the lowest energy 1453 conformation is written to the output file. 1454 --maxIters <number> [default: 500] 1455 Maximum number of iterations to perform for a molecule during minimization 1456 to generation initial 3D structures. This option is ignored during 'yes' value 1457 of '--infile3D' option. 1458 --mp <yes or no> [default: no] 1459 Use multiprocessing. 1460 1461 By default, input data is retrieved in a lazy manner via mp.Pool.imap() 1462 function employing lazy RDKit data iterable. This allows processing of 1463 arbitrary large data sets without any additional requirements memory. 1464 1465 All input data may be optionally loaded into memory by mp.Pool.map() 1466 before starting worker processes in a process pool by setting the value 1467 of 'inputDataMode' to 'InMemory' in '--mpParams' option. 1468 1469 A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input 1470 data mode may adversely impact the performance. The '--mpParams' section 1471 provides additional information to tune the value of 'chunkSize'. 1472 --mpLevel <Molecules or TorsionAngles> [default: Molecules] 1473 Perform multiprocessing at molecules or torsion angles level. Possible values: 1474 Molecules or TorsionAngles. The 'Molecules' value starts a process pool at the 1475 molecules level. All torsion angles of a molecule are processed in a single 1476 process. The 'TorsionAngles' value, however, starts a process pool at the 1477 torsion angles level. Each torsion angle in a torsion match for a molecule is 1478 processed in an individual process in the process pool. 1479 --mpParams <Name,Value,...> [default: auto] 1480 A comma delimited list of parameter name and value pairs to configure 1481 multiprocessing. 1482 1483 The supported parameter names along with their default and possible 1484 values are shown below: 1485 1486 chunkSize, auto 1487 inputDataMode, Lazy [ Possible values: InMemory or Lazy ] 1488 numProcesses, auto [ Default: mp.cpu_count() ] 1489 1490 These parameters are used by the following functions to configure and 1491 control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and 1492 mp.Pool.imap(). 1493 1494 The chunkSize determines chunks of input data passed to each worker 1495 process in a process pool by mp.Pool.map() and mp.Pool.imap() functions. 1496 The default value of chunkSize is dependent on the value of 'inputDataMode'. 1497 1498 The mp.Pool.map() function, invoked during 'InMemory' input data mode, 1499 automatically converts RDKit data iterable into a list, loads all data into 1500 memory, and calculates the default chunkSize using the following method 1501 as shown in its code: 1502 1503 chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4) 1504 if extra: chunkSize += 1 1505 1506 For example, the default chunkSize will be 7 for a pool of 4 worker processes 1507 and 100 data items. 1508 1509 The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs 1510 'lazy' RDKit data iterable to retrieve data as needed, without loading all the 1511 data into memory. Consequently, the size of input data is not known a priori. 1512 It's not possible to estimate an optimal value for the chunkSize. The default 1513 chunkSize is set to 1. 1514 1515 The default value for the chunkSize during 'Lazy' data mode may adversely 1516 impact the performance due to the overhead associated with exchanging 1517 small chunks of data. It is generally a good idea to explicitly set chunkSize to 1518 a larger value during 'Lazy' input data mode, based on the size of your input 1519 data and number of processes in the process pool. 1520 1521 The mp.Pool.map() function waits for all worker processes to process all 1522 the data and return the results. The mp.Pool.imap() function, however, 1523 returns the the results obtained from worker processes as soon as the 1524 results become available for specified chunks of data. 1525 1526 The order of data in the results returned by both mp.Pool.map() and 1527 mp.Pool.imap() functions always corresponds to the input data. 1528 -o, --outfile <outfile> 1529 Output file name. The output file root is used for generating the names 1530 of the output files corresponding to structures, energies, and plots during 1531 the torsion scan. 1532 --outfileMolName <yes or no> [default: no] 1533 Append molecule name to output file root during the generation of the names 1534 for output files. The default is to use <MolNum>. The non alphabetical 1535 characters in molecule names are replaced by underscores. 1536 --outfileParams <Name,Value,...> [default: auto] 1537 A comma delimited list of parameter name and value pairs for writing 1538 molecules to files. The supported parameter names for different file 1539 formats, along with their default values, are shown below: 1540 1541 SD: kekulize,yes,forceV3000,no 1542 1543 --outPlotParams <Name,Value,...> [default: auto] 1544 A comma delimited list of parameter name and value pairs for generating 1545 plots using Seaborn module. The supported parameter names along with their 1546 default values are shown below: 1547 1548 type,linepoint,outExt,svg,width,10,height,5.6, 1549 title,auto,xlabel,auto,ylabel,auto,titleWeight,bold,labelWeight,bold 1550 style,darkgrid,palette,deep,font,sans-serif,fontScale,1, 1551 context,notebook 1552 1553 Possible values: 1554 1555 type: linepoint, scatter, or line. Both points and lines are drawn 1556 for linepoint plot type. 1557 outExt: Any valid format supported by Python module Matplotlib. 1558 For example: PDF (.pdf), PNG (.png), PS (.ps), SVG (.svg) 1559 titleWeight, labelWeight: Font weight for title and axes labels. 1560 Any valid value. 1561 style: darkgrid, whitegrid, dark, white, ticks 1562 palette: deep, muted, pastel, dark, bright, colorblind 1563 font: Any valid font name 1564 context: paper, notebook, talk, poster, or any valid name 1565 1566 --outPlotRelativeEnergy <yes or no> [default: yes] 1567 Plot relative energies in the torsion plot. The minimum energy value is 1568 subtracted from energy values to calculate relative energies. 1569 --outPlotTitleTorsionSpec <yes or no> [default: yes] 1570 Append torsion specification to the title of the torsion plot. 1571 --overwrite 1572 Overwrite existing files. 1573 -q, --quiet <yes or no> [default: no] 1574 Use quiet mode. The warning and information messages will not be printed. 1575 --randomSeed <number> [default: auto] 1576 Seed for the random number generator for generating initial 3D coordinates. 1577 Default is to use a random seed. 1578 --removeHydrogens <yes or no> [default: Yes] 1579 Remove hydrogens after minimization. 1580 -t, --torsions <SMILES/SMARTS,...,...> 1581 SMILES/SMARTS patterns corresponding to torsion specifications. It's a 1582 comma delimited list of valid SMILES/SMART patterns. 1583 1584 A substructure match is performed to select torsion atoms in a molecule. 1585 The SMILES pattern match must correspond to four torsion atoms. The 1586 SMARTS patterns containing atom map numbers may match more than four 1587 atoms. The atom map numbers, however, must match exactly four torsion 1588 atoms. For example: [s:1][c:2]([aX2,cH1])!@[CX3:3](O)=[O:4] for thiophene 1589 esters and carboxylates as specified in Torsion Library (TorLib) [Ref 146]. 1590 --torsionsFilterbyAtomIndices <Index1, Index2, ...> [default: none] 1591 Comma delimited list of atom indices for filtering torsion matches 1592 corresponding to torsion specifications "-t, --torsions". The atom indices 1593 must be valid. No explicit validation is performed. The list must contain at 1594 least 4 atom indices. 1595 1596 The torsion atom indices, matched by "-t, --torsions" specifications, must be 1597 present in the list. Otherwise, the torsion matches are ignored. 1598 --torsionMaxMatches <number> [default: 5] 1599 Maximum number of torsions to match for each torsion specification in a 1600 molecule. 1601 --torsionMinimize <yes or no> [default: no] 1602 Perform constrained energy minimization on a conformation ensemble 1603 for a specific torsion angle and select the lowest energy conformation 1604 representing the torsion angle. 1605 --torsionRangeMode <Range or Angles> [default: Range] 1606 Perform torsion scan using torsion angles corresponding to a torsion range 1607 or an explicit list of torsion angles. Possible values: Range or Angles. You 1608 may use '--torsionRange' option to specify values for torsion angle or 1609 torsion angles. 1610 --torsionRange <Start,Stop,Step or Angle1,Angle2...> [default: auto] 1611 Start, stop, and step size angles or a comma delimited list of angles in 1612 degrees for a torsion scan. 1613 1614 This value is '--torsionRangeMode' specific. It must be a triplet corresponding 1615 to 'start,Stop,Step' for 'Range' value of '--torsionRange' option. Otherwise, it 1616 is comma delimited list of one or more torsion angles for 'Angles' value of 1617 '--torsionRange' option. 1618 1619 The default values, based on '--torsionRangeMode' option, are shown below: 1620 1621 TorsionRangeMode Default value 1622 Range 0,360,5 1623 Angles None 1624 1625 You must explicitly provide a list of torsion angle(s) for 'Angles' of 1626 '--torsionRangeMode' option. 1627 --useChirality <yes or no> [default: no] 1628 Use chirrality during substructure matches for identification of torsions. 1629 --useTethers <yes or no> [default: yes] 1630 Use tethers to optimize the final conformation by applying a series of extra forces 1631 to align matching atoms to the positions of the core atoms. Otherwise, use simple 1632 distance constraints during the optimization. 1633 -w, --workingdir <dir> 1634 Location of working directory which defaults to the current directory. 1635 1636 Examples: 1637 To perform a torsion scan from 0 to 360 degrees with a stepsize of 5 on the 1638 first molecule in a SMILES file using a minimum energy structure of the molecule 1639 selected from an ensemble of conformations, skipping generation of conformation 1640 ensembles for specific torsion angles and constrained energy minimization of the 1641 ensemble, generate output files corresponding to structure, energy and torsion 1642 plot, type: 1643 1644 % RDKitPerformTorsionScan.py -t "O=CNC" -i SampleSeriesD3R.smi 1645 -o SampleOut.sdf 1646 1647 To run the previous example for performing a torsion scan using a specific list 1648 of torsion angles, type: 1649 1650 % RDKitPerformTorsionScan.py -t "O=CNC" -i SampleSeriesD3R.smi 1651 -o SampleOut.sdf --torsionRangleMode Angles 1652 --torsionRange "160,220,280" 1653 1654 To run the previous example on all molecules in a SD file, type: 1655 1656 % RDKitPerformTorsionScan.py -t "O=CNC" --modeMols All 1657 -i SampleSeriesD3R.sdf -o SampleOut.sdf 1658 1659 To perform a torsion scan on the first molecule in a SMILES file using a minimum 1660 energy structure of the molecule selected from an ensemble of conformations, 1661 generation of conformation ensembles for specific torsion angles and constrained 1662 energy minimization of the ensemble, generate output files corresponding to 1663 structure, energy and torsion plot, type: 1664 1665 % RDKitPerformTorsionScan.py -t "O=CNC" --torsionMinimize Yes 1666 -i SampleSeriesD3R.smi -o SampleOut.sdf 1667 1668 To run the previous example on all molecules in a SD file, type: 1669 1670 % RDKitPerformTorsionScan.py -t "O=CNC" --modeMols All 1671 --torsionMinimize Yes -i SampleSeriesD3R.sdf -o SampleOut.sdf 1672 1673 To run the previous example in multiprocessing mode at molecules level 1674 on all available CPUs without loading all data into memory and write out 1675 a SD file, type: 1676 1677 % RDKitPerformTorsionScan.py -t "O=CNC" -i SampleSeriesD3R.smi 1678 -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes 1679 1680 To run the previous example in multiprocessing mode at torsion angles level 1681 on all available CPUs without loading all data into memory and write out 1682 a SD file, type: 1683 1684 % RDKitPerformTorsionScan.py -t "O=CNC" -i SampleSeriesD3R.smi 1685 -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes 1686 --mpLevel TorsionAngles 1687 1688 To run the previous example in multiprocessing mode on all available CPUs 1689 by loading all data into memory and write out a SD file, type: 1690 1691 % RDKitPerformTorsionScan.py -t "O=CNC" -i SampleSeriesD3R.smi 1692 -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes 1693 --mpParams "inputDataMode,InMemory" 1694 1695 To run the previous example in multiprocessing mode on specific number of 1696 CPUs and chunk size without loading all data into memory and write out a SD file, 1697 type: 1698 1699 % RDKitPerformTorsionScan.py -t "O=CNC" -i SampleSeriesD3R.smi 1700 -o SampleOut.sdf --modeMols All --torsionMinimize Yes --mp yes 1701 --mpParams "inputDataMode,Lazy,numProcesses,4,chunkSize,8" 1702 1703 To perform a torsion scan on first molecule in a SD file containing 3D coordinates, 1704 skipping generation of conformation ensembles for specific torsion angles and 1705 constrained energy minimization of the ensemble, generate output files 1706 corresponding to structure, energy and torsion plot, type: 1707 1708 % RDKitPerformTorsionScan.py -t "O=CNC" --infile3D yes 1709 -i SampleSeriesD3R3D.sdf -o SampleOut.sdf 1710 1711 To perform a torsion scan using multiple torsion specifications on all molecules in 1712 a SD file containing 3D coordinates, generation of conformation ensembles for specific 1713 torsion angles and constrained energy minimization of the ensemble, generate output files 1714 corresponding to structure, energy and torsion plot, type: 1715 1716 % RDKitPerformTorsionScan.py -t "O=CNC,[O:1]=[C:2](c)[N:3][C:4]" 1717 --infile3D yes --modeMols All --modeTorsions All 1718 --torsionMinimize Yes -i SampleSeriesD3R3D.sdf -o SampleOut.sdf 1719 1720 To run the previous example using a specific torsion scan range, type: 1721 1722 % RDKitPerformTorsionScan.py -t "O=CNC,[O:1]=[C:2](c)[N:3][C:4]" 1723 --infile3D yes --modeMols All --modeTorsions All --torsionMinimize 1724 Yes --torsionRange 0,360,10 -i SampleSeriesD3R.smi -o SampleOut.sdf 1725 1726 Author: 1727 Manish Sud(msud@san.rr.com) 1728 1729 See also: 1730 RDKitCalculateRMSD.py, RDKitCalculateMolecularDescriptors.py, RDKitCompareMoleculeShapes.py, 1731 RDKitConvertFileFormat.py, RDKitPerformConstrainedMinimization.py 1732 1733 Copyright: 1734 Copyright (C) 2024 Manish Sud. All rights reserved. 1735 1736 The functionality available in this script is implemented using RDKit, an 1737 open source toolkit for cheminformatics developed by Greg Landrum. 1738 1739 This file is part of MayaChemTools. 1740 1741 MayaChemTools is free software; you can redistribute it and/or modify it under 1742 the terms of the GNU Lesser General Public License as published by the Free 1743 Software Foundation; either version 3 of the License, or (at your option) any 1744 later version. 1745 1746 """ 1747 1748 if __name__ == "__main__": 1749 main()