1 #!/bin/env python
2 #
3 # File: RDKitGenerateConformers.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 from rdkit.Chem import Descriptors
43 except ImportError as ErrMsg:
44 sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
45 sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
46 sys.exit(1)
47
48 # MayaChemTools imports...
49 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
50 try:
51 from docopt import docopt
52 import MiscUtil
53 import RDKitUtil
54 except ImportError as ErrMsg:
55 sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
56 sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
57 sys.exit(1)
58
59 ScriptName = os.path.basename(sys.argv[0])
60 Options = {}
61 OptionsInfo = {}
62
63
64 def main():
65 """Start execution of the script."""
66
67 MiscUtil.PrintInfo(
68 "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
69 % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
70 )
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 GenerateConformers()
82
83 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
84 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
85
86
87 def GenerateConformers():
88 """Generate conformers."""
89
90 # Setup a molecule reader...
91 MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
92 Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
93
94 # Set up a molecule writer...
95 Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
96 if Writer is None:
97 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
98 MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"])
99
100 MolCount, ValidMolCount, ConfGenFailedCount = ProcessMolecules(Mols, Writer)
101
102 if Writer is not None:
103 Writer.close()
104
105 MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
106 MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
107 MiscUtil.PrintInfo(
108 "Number of molecules failed during conformation generation or minimization: %d" % ConfGenFailedCount
109 )
110 MiscUtil.PrintInfo("Number of ignored molecules: %d" % (MolCount - ValidMolCount + ConfGenFailedCount))
111
112
113 def ProcessMolecules(Mols, Writer):
114 """Process molecules to generate conformers."""
115
116 if OptionsInfo["MPMode"]:
117 return ProcessMoleculesUsingMultipleProcesses(Mols, Writer)
118 else:
119 return ProcessMoleculesUsingSingleProcess(Mols, Writer)
120
121
122 def ProcessMoleculesUsingSingleProcess(Mols, Writer):
123 """Process molecules to generate conformers using a single process."""
124
125 if OptionsInfo["SkipForceFieldMinimization"]:
126 MiscUtil.PrintInfo("\nGenerating conformers without performing energy minimization...")
127 else:
128 MiscUtil.PrintInfo("\nGenerating conformers and performing energy minimization...")
129
130 (MolCount, ValidMolCount, ConfGenFailedCount) = [0] * 3
131 for Mol in Mols:
132 MolCount += 1
133
134 if Mol is None:
135 continue
136
137 if RDKitUtil.IsMolEmpty(Mol):
138 if not OptionsInfo["QuietMode"]:
139 MolName = RDKitUtil.GetMolName(Mol, MolCount)
140 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
141 continue
142 ValidMolCount += 1
143
144 ConformerMol, CalcStatus, ConfIDs, ConfEnergies = GenerateMolConformers(Mol, MolCount)
145
146 if not CalcStatus:
147 ConfGenFailedCount += 1
148 continue
149
150 WriteMolConformers(Writer, ConformerMol, MolCount, ConfIDs, ConfEnergies)
151
152 return (MolCount, ValidMolCount, ConfGenFailedCount)
153
154
155 def ProcessMoleculesUsingMultipleProcesses(Mols, Writer):
156 """Process and minimize molecules using multiprocessing."""
157
158 if OptionsInfo["SkipForceFieldMinimization"]:
159 MiscUtil.PrintInfo("\nGenerating conformers without performing energy minimization using multiprocessing...")
160 else:
161 MiscUtil.PrintInfo("\nGenerating conformers and performing energy minimization using multiprocessing...")
162
163 MPParams = OptionsInfo["MPParams"]
164
165 # Setup data for initializing a worker process...
166 InitializeWorkerProcessArgs = (
167 MiscUtil.ObjectToBase64EncodedString(Options),
168 MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
169 )
170
171 # Setup a encoded mols data iterable for a worker process...
172 WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
173
174 # Setup process pool along with data initialization for each process...
175 MiscUtil.PrintInfo(
176 "\nConfiguring multiprocessing using %s method..."
177 % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
178 )
179 MiscUtil.PrintInfo(
180 "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
181 % (
182 MPParams["NumProcesses"],
183 MPParams["InputDataMode"],
184 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
185 )
186 )
187
188 ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
189
190 # Start processing...
191 if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
192 Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
193 elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
194 Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
195 else:
196 MiscUtil.PrintError(
197 'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
198 )
199
200 (MolCount, ValidMolCount, ConfGenFailedCount) = [0] * 3
201 for Result in Results:
202 MolCount += 1
203 MolIndex, EncodedMol, CalcStatus, ConfIDs, ConfEnergies = Result
204
205 if EncodedMol is None:
206 continue
207 ValidMolCount += 1
208
209 if not CalcStatus:
210 ConfGenFailedCount += 1
211 continue
212
213 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
214 WriteMolConformers(Writer, Mol, MolCount, ConfIDs, ConfEnergies)
215
216 return (MolCount, ValidMolCount, ConfGenFailedCount)
217
218
219 def InitializeWorkerProcess(*EncodedArgs):
220 """Initialize data for a worker process."""
221
222 global Options, OptionsInfo
223
224 MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
225
226 # Decode Options and OptionInfo...
227 Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
228 OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
229
230
231 def WorkerProcess(EncodedMolInfo):
232 """Process data for a worker process."""
233
234 MolIndex, EncodedMol = EncodedMolInfo
235
236 CalcStatus = False
237 ConfIDs = None
238 ConfEnergies = None
239
240 if EncodedMol is None:
241 return [MolIndex, None, CalcStatus, ConfIDs, ConfEnergies]
242
243 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
244 if RDKitUtil.IsMolEmpty(Mol):
245 if not OptionsInfo["QuietMode"]:
246 MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
247 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
248 return [MolIndex, None, CalcStatus, ConfIDs, ConfEnergies]
249
250 Mol, CalcStatus, ConfIDs, ConfEnergies = GenerateMolConformers(Mol, (MolIndex + 1))
251
252 return [
253 MolIndex,
254 RDKitUtil.MolToBase64EncodedMolString(
255 Mol, PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps
256 ),
257 CalcStatus,
258 ConfIDs,
259 ConfEnergies,
260 ]
261
262
263 def GenerateMolConformers(Mol, MolNum=None):
264 """Generate conformers for a molecule."""
265
266 if OptionsInfo["SkipForceFieldMinimization"]:
267 return GenerateMolConformersWithoutMinimization(Mol, MolNum)
268 else:
269 return GenerateMolConformersWithMinimization(Mol, MolNum)
270
271
272 def GenerateMolConformersWithoutMinimization(Mol, MolNum=None):
273 """Generate conformers for a molecule without performing minimization."""
274
275 ConfIDs = EmbedMolecule(Mol, MolNum)
276 if not len(ConfIDs):
277 if not OptionsInfo["QuietMode"]:
278 MolName = RDKitUtil.GetMolName(Mol, MolNum)
279 MiscUtil.PrintWarning(
280 "Conformation generation couldn't be performed for molecule %s: Embedding failed...\n" % MolName
281 )
282 return [Mol, False, None, None]
283
284 if OptionsInfo["AlignConformers"]:
285 AllChem.AlignMolConformers(Mol)
286
287 if not OptionsInfo["QuietMode"]:
288 MolName = RDKitUtil.GetMolName(Mol, MolNum)
289 MiscUtil.PrintInfo("\nNumber of conformations generated for %s: %d" % (MolName, len(ConfIDs)))
290
291 # Convert ConfIDs into a list...
292 ConfIDsList = [ConfID for ConfID in ConfIDs]
293
294 # Setup conformation energies...
295 ConfEnergies = None
296 if OptionsInfo["EnergyOut"]:
297 ConfEnergies = []
298 for ConfID in ConfIDsList:
299 EnergyStatus, Energy = GetConformerEnergy(Mol, ConfID)
300
301 Energy = "%.2f" % Energy if EnergyStatus else "NotAvailable"
302 ConfEnergies.append(Energy)
303
304 if not EnergyStatus:
305 if not OptionsInfo["QuietMode"]:
306 MolName = RDKitUtil.GetMolName(Mol, MolNum)
307 MiscUtil.PrintWarning(
308 "Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n"
309 % (ConfID, MolName)
310 )
311
312 return [Mol, True, ConfIDsList, ConfEnergies]
313
314
315 def GenerateMolConformersWithMinimization(Mol, MolNum):
316 """Generate and mininize conformers for a molecule."""
317
318 if OptionsInfo["AddHydrogens"]:
319 Mol = Chem.AddHs(Mol)
320
321 ConfIDs = EmbedMolecule(Mol, MolNum)
322 if not len(ConfIDs):
323 if not OptionsInfo["QuietMode"]:
324 MolName = RDKitUtil.GetMolName(Mol, MolNum)
325 MiscUtil.PrintWarning(
326 "Conformation generation couldn't be performed for molecule %s: Embedding failed...\n" % MolName
327 )
328 return [Mol, False, None, None]
329
330 CalcEnergyMap = {}
331 for ConfID in ConfIDs:
332 try:
333 if OptionsInfo["UseUFF"]:
334 Status = AllChem.UFFOptimizeMolecule(Mol, confId=ConfID, maxIters=OptionsInfo["MaxIters"])
335 elif OptionsInfo["UseMMFF"]:
336 Status = AllChem.MMFFOptimizeMolecule(
337 Mol, confId=ConfID, maxIters=OptionsInfo["MaxIters"], mmffVariant=OptionsInfo["MMFFVariant"]
338 )
339 else:
340 MiscUtil.PrintError(
341 "Minimization couldn't be performed: Specified forcefield, %s, is not supported"
342 % OptionsInfo["ForceField"]
343 )
344 except (RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg:
345 if not OptionsInfo["QuietMode"]:
346 MolName = RDKitUtil.GetMolName(Mol, MolNum)
347 MiscUtil.PrintWarning("Minimization couldn't be performed for molecule %s:\n%s\n" % (MolName, ErrMsg))
348 return [Mol, False, None, None]
349
350 EnergyStatus, Energy = GetConformerEnergy(Mol, ConfID)
351 if not EnergyStatus:
352 if not OptionsInfo["QuietMode"]:
353 MolName = RDKitUtil.GetMolName(Mol, MolNum)
354 MiscUtil.PrintWarning(
355 "Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n"
356 % (ConfID, MolName)
357 )
358 return [Mol, False, None, None]
359
360 if Status != 0:
361 if not OptionsInfo["QuietMode"]:
362 MolName = RDKitUtil.GetMolName(Mol, MolNum)
363 MiscUtil.PrintWarning(
364 'Minimization failed to converge for conformation number %d of molecule %s in %d steps. Try using higher value for "--maxIters" option...\n'
365 % (ConfID, MolName, OptionsInfo["MaxIters"])
366 )
367
368 CalcEnergyMap[ConfID] = Energy
369
370 if OptionsInfo["RemoveHydrogens"]:
371 Mol = Chem.RemoveHs(Mol)
372
373 # Align molecules after minimization...
374 if OptionsInfo["AlignConformers"]:
375 AllChem.AlignMolConformers(Mol)
376
377 SortedConfIDs = sorted(ConfIDs, key=lambda ConfID: CalcEnergyMap[ConfID])
378
379 MinEnergyConfID = SortedConfIDs[0]
380 MinConfEnergy = CalcEnergyMap[MinEnergyConfID]
381 EnergyWindow = OptionsInfo["EnergyWindow"]
382
383 EnergyRMSDCutoff = OptionsInfo["EnergyRMSDCutoff"]
384 ApplyEnergyRMSDCutoff = False
385 if EnergyRMSDCutoff > 0:
386 ApplyEnergyRMSDCutoff = True
387
388 EnergyRMSDCutoffLowest = OptionsInfo["EnergyRMSDCutoffModeLowest"]
389 EnergyRMSDCalcModeBest = OptionsInfo["EnergyRMSDCalcModeBest"]
390
391 PreAligned = False
392 if OptionsInfo["AlignConformers"]:
393 PreAligned = True
394
395 RefMol, ProbeMol = [None] * 2
396 if EnergyRMSDCalcModeBest:
397 # Copy molecules for best RMSD calculations to avoid change in the coordinates
398 # of the conformations...
399 RefMol = AllChem.Mol(Mol)
400 ProbeMol = AllChem.Mol(Mol)
401
402 # Track conformers with in the specified energy window from the lowest
403 # energy conformation along with applying RMSD cutoff as needed...
404 #
405 SelectedConfIDs = []
406
407 ConfCount = 0
408 IgnoredByEnergyConfCount = 0
409 IgnoredByRMSDConfCount = 0
410
411 FirstConf = True
412 for ConfID in SortedConfIDs:
413 if FirstConf:
414 FirstConf = False
415 ConfCount += 1
416 SelectedConfIDs.append(ConfID)
417 continue
418
419 ConfEnergyDiff = abs(CalcEnergyMap[ConfID] - MinConfEnergy)
420 if ConfEnergyDiff > EnergyWindow:
421 IgnoredByEnergyConfCount += 1
422 continue
423
424 if ApplyEnergyRMSDCutoff:
425 IgnoreConf = False
426 if EnergyRMSDCutoffLowest:
427 # Compare RMSD with the lowest energy conformation...
428 if EnergyRMSDCalcModeBest:
429 CalcRMSD = AllChem.GetBestRMS(ProbeMol, RefMol, prbId=ConfID, refId=MinEnergyConfID)
430 else:
431 CalcRMSD = AllChem.GetConformerRMS(Mol, MinEnergyConfID, ConfID, prealigned=PreAligned)
432 if CalcRMSD < EnergyRMSDCutoff:
433 IgnoreConf = True
434 else:
435 for SelectedConfID in SelectedConfIDs:
436 if EnergyRMSDCalcModeBest:
437 CalcRMSD = AllChem.GetBestRMS(ProbeMol, RefMol, prbId=ConfID, refId=SelectedConfID)
438 else:
439 CalcRMSD = AllChem.GetConformerRMS(Mol, SelectedConfID, ConfID, prealigned=PreAligned)
440 if CalcRMSD < EnergyRMSDCutoff:
441 IgnoreConf = True
442 break
443 if IgnoreConf:
444 IgnoredByRMSDConfCount += 1
445 continue
446
447 ConfCount += 1
448 SelectedConfIDs.append(ConfID)
449
450 if not OptionsInfo["QuietMode"]:
451 MolName = RDKitUtil.GetMolName(Mol, MolNum)
452 MiscUtil.PrintInfo("\nTotal Number of conformations generated for %s: %d" % (MolName, ConfCount))
453 MiscUtil.PrintInfo(
454 "Number of conformations ignored due to energy window cutoff: %d" % (IgnoredByEnergyConfCount)
455 )
456 if ApplyEnergyRMSDCutoff:
457 MiscUtil.PrintInfo(
458 "Number of conformations ignored due to energy RMSD cutoff: %d" % (IgnoredByRMSDConfCount)
459 )
460
461 SelectedConfEnergies = None
462 if OptionsInfo["EnergyOut"]:
463 SelectedConfEnergies = ["%.2f" % CalcEnergyMap[ConfID] for ConfID in SelectedConfIDs]
464
465 return [Mol, True, SelectedConfIDs, SelectedConfEnergies]
466
467
468 def GetConformerEnergy(Mol, ConfID):
469 """Calculate conformer energy."""
470
471 Status = True
472 Energy = 0.0
473
474 if OptionsInfo["UseUFF"]:
475 UFFMoleculeForcefield = AllChem.UFFGetMoleculeForceField(Mol, confId=ConfID)
476 if UFFMoleculeForcefield is None:
477 Status = False
478 else:
479 Energy = UFFMoleculeForcefield.CalcEnergy()
480 elif OptionsInfo["UseMMFF"]:
481 MMFFMoleculeProperties = AllChem.MMFFGetMoleculeProperties(Mol, mmffVariant=OptionsInfo["MMFFVariant"])
482 MMFFMoleculeForcefield = AllChem.MMFFGetMoleculeForceField(Mol, MMFFMoleculeProperties, confId=ConfID)
483 if MMFFMoleculeForcefield is None:
484 Status = False
485 else:
486 Energy = MMFFMoleculeForcefield.CalcEnergy()
487 else:
488 MiscUtil.PrintError(
489 "Couldn't retrieve conformer energy: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"]
490 )
491
492 return (Status, Energy)
493
494
495 def EmbedMolecule(Mol, MolNum=None):
496 """Embed conformations."""
497
498 ConfIDs = []
499
500 # Figure out the number of conformations to embded...
501 if re.match("^Auto$", OptionsInfo["MaxConfs"], re.I):
502 NumOfRotBonds = Descriptors.NumRotatableBonds(Mol)
503 if NumOfRotBonds <= 5:
504 MaxConfs = 100
505 elif NumOfRotBonds >= 6 and NumOfRotBonds <= 10:
506 MaxConfs = 200
507 else:
508 MaxConfs = 300
509 else:
510 MaxConfs = int(OptionsInfo["MaxConfs"])
511
512 RandomSeed = OptionsInfo["RandomSeed"]
513 EnforceChirality = OptionsInfo["EnforceChirality"]
514 UseExpTorsionAnglePrefs = OptionsInfo["UseExpTorsionAnglePrefs"]
515 UseBasicKnowledge = OptionsInfo["UseBasicKnowledge"]
516 ETVersion = OptionsInfo["ETVersion"]
517 EmbedRMSDCutoff = OptionsInfo["EmbedRMSDCutoff"]
518
519 try:
520 ConfIDs = AllChem.EmbedMultipleConfs(
521 Mol,
522 numConfs=MaxConfs,
523 randomSeed=RandomSeed,
524 pruneRmsThresh=EmbedRMSDCutoff,
525 enforceChirality=EnforceChirality,
526 useExpTorsionAnglePrefs=UseExpTorsionAnglePrefs,
527 useBasicKnowledge=UseBasicKnowledge,
528 ETversion=ETVersion,
529 )
530 except ValueError as ErrMsg:
531 if not OptionsInfo["QuietMode"]:
532 MolName = RDKitUtil.GetMolName(Mol, MolNum)
533 MiscUtil.PrintWarning("Embedding failed for molecule %s:\n%s\n" % (MolName, ErrMsg))
534 ConfIDs = []
535
536 if not OptionsInfo["QuietMode"]:
537 if EmbedRMSDCutoff > 0:
538 MiscUtil.PrintInfo(
539 "Generating initial conformation ensemble by distance geometry for %s - EmbedRMSDCutoff: %s; Size: %s; Size after RMSD filtering: %s"
540 % (RDKitUtil.GetMolName(Mol, MolNum), EmbedRMSDCutoff, MaxConfs, len(ConfIDs))
541 )
542 else:
543 MiscUtil.PrintInfo(
544 "Generating initial conformation ensemble by distance geometry for %s - EmbedRMSDCutoff: None; Size: %s"
545 % (RDKitUtil.GetMolName(Mol, MolNum), len(ConfIDs))
546 )
547
548 return ConfIDs
549
550
551 def WriteMolConformers(Writer, Mol, MolNum, ConfIDs, ConfEnergies=None):
552 """Write molecule coformers."""
553
554 if ConfIDs is None:
555 return
556
557 MolName = RDKitUtil.GetMolName(Mol, MolNum)
558
559 for Index, ConfID in enumerate(ConfIDs):
560 SetConfMolName(Mol, MolName, ConfID)
561
562 if ConfEnergies is not None:
563 Mol.SetProp(OptionsInfo["EnergyLabel"], ConfEnergies[Index])
564
565 Writer.write(Mol, confId=ConfID)
566
567
568 def SetConfMolName(Mol, MolName, ConfCount):
569 """Set conf mol name."""
570
571 ConfName = "%s_Conf%d" % (MolName, ConfCount)
572 Mol.SetProp("_Name", ConfName)
573
574
575 def ProcesssConformerGeneratorOption():
576 """Process comformer generator option."""
577
578 ConfGenParams = MiscUtil.ProcessOptionConformerGenerator("--conformerGenerator", Options["--conformerGenerator"])
579
580 OptionsInfo["ConformerGenerator"] = ConfGenParams["ConformerGenerator"]
581 OptionsInfo["UseBasicKnowledge"] = ConfGenParams["UseBasicKnowledge"]
582 OptionsInfo["UseExpTorsionAnglePrefs"] = ConfGenParams["UseExpTorsionAnglePrefs"]
583 OptionsInfo["ETVersion"] = ConfGenParams["ETVersion"]
584
585
586 def ProcessOptions():
587 """Process and validate command line arguments and options."""
588
589 MiscUtil.PrintInfo("Processing options...")
590
591 # Validate options...
592 ValidateOptions()
593
594 OptionsInfo["Infile"] = Options["--infile"]
595 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
596 "--infileParams", Options["--infileParams"], Options["--infile"]
597 )
598
599 OptionsInfo["Outfile"] = Options["--outfile"]
600 OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
601 "--outfileParams", Options["--outfileParams"]
602 )
603
604 OptionsInfo["Overwrite"] = Options["--overwrite"]
605
606 OptionsInfo["AddHydrogens"] = True
607 if re.match("^no$", Options["--addHydrogens"], re.I):
608 OptionsInfo["AddHydrogens"] = False
609
610 OptionsInfo["AlignConformers"] = True
611 if re.match("^no$", Options["--alignConformers"], re.I):
612 OptionsInfo["AlignConformers"] = False
613
614 ProcesssConformerGeneratorOption()
615
616 if re.match("^UFF$", Options["--forceField"], re.I):
617 ForceField = "UFF"
618 UseUFF = True
619 UseMMFF = False
620 SkipForceFieldMinimization = False
621 elif re.match("^MMFF$", Options["--forceField"], re.I):
622 ForceField = "MMFF"
623 UseUFF = False
624 UseMMFF = True
625 SkipForceFieldMinimization = False
626 else:
627 ForceField = "None"
628 UseUFF = False
629 UseMMFF = False
630 SkipForceFieldMinimization = True
631
632 MMFFVariant = "MMFF94" if re.match("^MMFF94$", Options["--forceFieldMMFFVariant"], re.I) else "MMFF94s"
633
634 OptionsInfo["SkipForceFieldMinimization"] = SkipForceFieldMinimization
635 OptionsInfo["ForceField"] = ForceField
636 OptionsInfo["MMFFVariant"] = MMFFVariant
637 OptionsInfo["UseMMFF"] = UseMMFF
638 OptionsInfo["UseUFF"] = UseUFF
639
640 OptionsInfo["EnergyOut"] = True if re.match("^yes$", Options["--energyOut"], re.I) else False
641 if UseUFF:
642 EnergyLabel = "UFF_Energy"
643 elif UseMMFF:
644 EnergyLabel = "%s_Energy" % MMFFVariant
645 else:
646 EnergyLabel = "Energy"
647 OptionsInfo["EnergyLabel"] = EnergyLabel
648
649 OptionsInfo["EnforceChirality"] = True
650 if re.match("^no$", Options["--enforceChirality"], re.I):
651 OptionsInfo["EnforceChirality"] = False
652
653 OptionsInfo["EnergyWindow"] = float(Options["--energyWindow"])
654
655 EmbedRMSDCutoff = -1.0
656 if not re.match("^none$", Options["--embedRMSDCutoff"], re.I):
657 EmbedRMSDCutoff = float(Options["--embedRMSDCutoff"])
658 OptionsInfo["EmbedRMSDCutoff"] = EmbedRMSDCutoff
659
660 OptionsInfo["EnergyRMSDCalcMode"] = Options["--energyRMSDCalcMode"]
661 OptionsInfo["EnergyRMSDCalcModeBest"] = (
662 True if re.match("^BestRMSD$", Options["--energyRMSDCalcMode"], re.I) else False
663 )
664
665 EnergyRMSDCutoff = -1.0
666 if not re.match("^none$", Options["--energyRMSDCutoff"], re.I):
667 EnergyRMSDCutoff = float(Options["--energyRMSDCutoff"])
668 OptionsInfo["EnergyRMSDCutoff"] = EnergyRMSDCutoff
669
670 OptionsInfo["EnergyRMSDCutoffMode"] = Options["--energyRMSDCutoffMode"]
671 OptionsInfo["EnergyRMSDCutoffModeLowest"] = (
672 True if re.match("^Lowest$", Options["--energyRMSDCutoffMode"], re.I) else False
673 )
674
675 OptionsInfo["MaxIters"] = int(Options["--maxIters"])
676 OptionsInfo["MaxConfs"] = Options["--maxConfs"]
677
678 OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
679 OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
680
681 OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False
682
683 RandomSeed = -1
684 if not re.match("^auto$", Options["--randomSeed"], re.I):
685 RandomSeed = int(Options["--randomSeed"])
686 OptionsInfo["RandomSeed"] = RandomSeed
687
688 OptionsInfo["RemoveHydrogens"] = True
689 if re.match("^no$", Options["--removeHydrogens"], re.I):
690 OptionsInfo["RemoveHydrogens"] = False
691
692
693 def RetrieveOptions():
694 """Retrieve command line arguments and options."""
695
696 # Get options...
697 global Options
698 Options = docopt(_docoptUsage_)
699
700 # Set current working directory to the specified directory...
701 WorkingDir = Options["--workingdir"]
702 if WorkingDir:
703 os.chdir(WorkingDir)
704
705 # Handle examples option...
706 if "--examples" in Options and Options["--examples"]:
707 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
708 sys.exit(0)
709
710
711 def ValidateOptions():
712 """Validate option values."""
713
714 MiscUtil.ValidateOptionTextValue("-a, --addHydrogens", Options["--addHydrogens"], "yes no")
715 MiscUtil.ValidateOptionTextValue("--alignConformers", Options["--alignConformers"], "yes no")
716 MiscUtil.ValidateOptionTextValue(
717 "-c, --conformerGenerator", Options["--conformerGenerator"], "SDG KDG ETDG ETKDG ETKDGv2"
718 )
719
720 MiscUtil.ValidateOptionTextValue("-f, --forceField", Options["--forceField"], "UFF MMFF None")
721 MiscUtil.ValidateOptionTextValue(" --forceFieldMMFFVariant", Options["--forceFieldMMFFVariant"], "MMFF94 MMFF94s")
722
723 MiscUtil.ValidateOptionTextValue("--energyOut", Options["--energyOut"], "yes no")
724 MiscUtil.ValidateOptionTextValue("--enforceChirality ", Options["--enforceChirality"], "yes no")
725 MiscUtil.ValidateOptionFloatValue("--energyWindow", Options["--energyWindow"], {">": 0.0})
726
727 MiscUtil.ValidateOptionTextValue(" --energyRMSDCalcMode", Options["--energyRMSDCalcMode"], "RMSD BestRMSD")
728
729 if not re.match("^none$", Options["--embedRMSDCutoff"], re.I):
730 MiscUtil.ValidateOptionFloatValue("--embedRMSDCutoff", Options["--embedRMSDCutoff"], {">": 0.0})
731 MiscUtil.ValidateOptionTextValue(" --energyRMSDCutoffMode", Options["--energyRMSDCutoffMode"], "All Lowest")
732
733 if not re.match("^none$", Options["--energyRMSDCutoff"], re.I):
734 MiscUtil.ValidateOptionFloatValue("--energyRMSDCutoff", Options["--energyRMSDCutoff"], {">": 0.0})
735 # Make sure that the alignConformers option is being used...
736 if not re.match("^yes$", Options["--alignConformers"], re.I):
737 MiscUtil.PrintError(
738 '"%s" value of "--alignConformers" is not allowed for %s value of "--energyRMSDCutoff" option. It must be set to "yes".'
739 % (Options["--alignConformers"], Options["--energyRMSDCutoff"])
740 )
741
742 MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
743 MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv")
744
745 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd")
746 MiscUtil.ValidateOptionsOutputFileOverwrite(
747 "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
748 )
749 MiscUtil.ValidateOptionsDistinctFileNames(
750 "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
751 )
752
753 if not re.match("^auto$", Options["--maxConfs"], re.I):
754 MiscUtil.ValidateOptionIntegerValue("--maxConfs", Options["--maxConfs"], {">": 0})
755
756 MiscUtil.ValidateOptionIntegerValue("--maxIters", Options["--maxIters"], {">": 0})
757
758 MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
759 MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no")
760
761 if not re.match("^auto$", Options["--randomSeed"], re.I):
762 MiscUtil.ValidateOptionIntegerValue("--randomSeed", Options["--randomSeed"], {})
763
764 MiscUtil.ValidateOptionTextValue("-r, --removeHydrogens", Options["--removeHydrogens"], "yes no")
765
766
767 # Setup a usage string for docopt...
768 _docoptUsage_ = """
769 RDKitGenerateConformers.py - Generate molecular conformations
770
771 Usage:
772 RDKitGenerateConformers.py [--alignConformers <yes or no>] [--addHydrogens <yes or no>]
773 [--conformerGenerator <SDG, ETDG, KDG, ETKDG, ETKDGv2>] [--embedRMSDCutoff <number>]
774 [--energyOut <yes or no>] [--enforceChirality <yes or no>] [--energyRMSDCalcMode <RMSD or BestRMSD>]
775 [--energyRMSDCutoff <number>] [--energyRMSDCutoffMode <All or Lowest>] [--energyWindow <number>]
776 [--forceField <UFF, MMFF, None>] [--forceFieldMMFFVariant <MMFF94 or MMFF94s>]
777 [--infileParams <Name,Value,...>] [--maxConfs <number>]
778 [--mp <yes or no>] [--mpParams <Name,Value,...>]
779 [--maxIters <number>] [ --outfileParams <Name,Value,...> ] [--overwrite]
780 [--quiet <yes or no>] [ --removeHydrogens <yes or no>] [--randomSeed <number>]
781 [-w <dir>] -i <infile> -o <outfile>
782 RDKitGenerateConformers.py -h | --help | -e | --examples
783
784 Description:
785 Generate 3D conformes of molecules using a combination of distance geometry and
786 forcefield minimization. The forcefield minimization may be skipped to only generate
787 conformations by available distance geometry based methodologies.
788
789 The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
790 .csv, .tsv, .txt)
791
792 The supported output file format are: SD (.sdf, .sd)
793
794 Options:
795 -a, --addHydrogens <yes or no> [default: yes]
796 Add hydrogens before minimization.
797 --alignConformers <yes or no> [default: yes]
798 Align conformers for each molecule.
799 -c, --conformerGenerator <text> [default: ETKDGv2]
800 Conformation generation methodology for generating initial 3D coordinates. The
801 possible values along with a brief description are shown below:
802
803 SDG: Standard Distance Geometry
804 KDG: basic Knowledge-terms with Distance Geometry
805 ETDG: Experimental Torsion-angle preference with Distance Geometry
806 ETKDG: Experimental Torsion-angle preference along with basic
807 Knowledge-terms and Distance Geometry [Ref 129]
808 ETKDGv2: Experimental Torsion-angle preference along with basic
809 Knowledge-terms and Distance Geometry [Ref 167]
810 --embedRMSDCutoff <number> [default: none]
811 RMSD cutoff for retaining conformations after embedding and before energy minimization.
812 All embedded conformations are kept by default. Otherwise, only those conformations
813 which are different from each other by the specified RMSD cutoff are kept. The first
814 embedded conformation is always retained.
815 --energyOut <yes or no> [default: No]
816 Write out energy values.
817 --enforceChirality <yes or no> [default: Yes]
818 Enforce chirality for defined chiral centers.
819 --energyRMSDCalcMode <RMSD or BestRMSD> [default: RMSD]
820 Methodology for calculating RMSD values during the application of RMSD
821 cutoff for retaining conformations after energy minimization. Possible
822 values: RMSD or BestRMSD. This option is ignore during 'None' value of
823 '--energyRMSDCutoff' option.
824
825 During BestRMSMode mode, the RDKit 'function AllChem.GetBestRMS' is used to
826 align and calculate RMSD. This function calculates optimal RMSD for aligning two
827 molecules, taking symmetry into account. Otherwise, the RMSD value is calculated
828 using 'AllChem.GetConformerRMS' without changing the atom order. A word to the
829 wise from RDKit documentation: The AllChem.GetBestRMS function will attempt to
830 align all permutations of matching atom orders in both molecules, for some molecules
831 it will lead to 'combinatorial explosion'.
832 --energyRMSDCutoff <number> [default: none]
833 RMSD cutoff for retaining conformations after energy minimization. By default,
834 all minimized conformations with in the specified energy window from the lowest
835 energy conformation are kept. Otherwise, only those conformations which are
836 different from the lowest energy conformation or all selected conformations
837 by the specified RMSD cutoff and are with in the specified energy window are
838 kept. The lowest energy conformation is always retained.
839 --energyRMSDCutoffMode <All or Lowest> [default: All]
840 RMSD cutoff mode for retaining conformations after energy minimization.
841 Possible values: All or Lowest. The RMSD values are compared against all
842 the selected conformations or the lowest energy conformation during 'All'
843 and 'Lowest' value of '--energyRMSDCutoffMode'. This option is ignored
844 during 'None' value of '--energyRMSDCutoff' option.
845
846 By default, only those conformations which all different from all selected
847 conformations by the specified RMSD cutoff and are with in the specified
848 energy window are kept.
849 --energyWindow <number> [default: 20]
850 Energy window in kcal/mol for selecting conformers. This option is ignored during
851 'None' value of '-f, --forcefield' option.
852 -e, --examples
853 Print examples.
854 -f, --forceField <UFF, MMFF, None> [default: MMFF]
855 Forcefield method to use for energy minimization. Possible values: Universal Force
856 Field (UFF) [Ref 81], Merck Molecular Mechanics Force Field (MMFF) [Ref 83-87] or
857 None.
858 --forceFieldMMFFVariant <MMFF94 or MMFF94s> [default: MMFF94]
859 Variant of MMFF forcefield to use for energy minimization.
860 -h, --help
861 Print this help message.
862 -i, --infile <infile>
863 Input file name.
864 --infileParams <Name,Value,...> [default: auto]
865 A comma delimited list of parameter name and value pairs for reading
866 molecules from files. The supported parameter names for different file
867 formats, along with their default values, are shown below:
868
869 SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
870 SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
871 smilesTitleLine,auto,sanitize,yes
872
873 Possible values for smilesDelimiter: space, comma or tab.
874 --maxConfs <number> [default: auto]
875 Maximum number of conformations to generate for each molecule by conformation
876 generation methodology. The conformations are minimized using the specified
877 forcefield as needed and written to the output file. The default value for maximum
878 number of conformations is dependent on the number of rotatable bonds in molecules:
879 RotBonds <= 5, maxConfs = 100; RotBonds >=6 and <= 10, MaxConfs = 200;
880 RotBonds >= 11, maxConfs = 300
881 --maxIters <number> [default: 250]
882 Maximum number of iterations to perform for each molecule during forcefield
883 minimization.
884 --mp <yes or no> [default: no]
885 Use multiprocessing.
886
887 By default, input data is retrieved in a lazy manner via mp.Pool.imap()
888 function employing lazy RDKit data iterable. This allows processing of
889 arbitrary large data sets without any additional requirements memory.
890
891 All input data may be optionally loaded into memory by mp.Pool.map()
892 before starting worker processes in a process pool by setting the value
893 of 'inputDataMode' to 'InMemory' in '--mpParams' option.
894
895 A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
896 data mode may adversely impact the performance. The '--mpParams' section
897 provides additional information to tune the value of 'chunkSize'.
898 --mpParams <Name,Value,...> [default: auto]
899 A comma delimited list of parameter name and value pairs to configure
900 multiprocessing.
901
902 The supported parameter names along with their default and possible
903 values are shown below:
904
905 chunkSize, auto
906 inputDataMode, Lazy [ Possible values: InMemory or Lazy ]
907 numProcesses, auto [ Default: mp.cpu_count() ]
908
909 These parameters are used by the following functions to configure and
910 control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
911 mp.Pool.imap().
912
913 The chunkSize determines chunks of input data passed to each worker
914 process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
915 The default value of chunkSize is dependent on the value of 'inputDataMode'.
916
917 The mp.Pool.map() function, invoked during 'InMemory' input data mode,
918 automatically converts RDKit data iterable into a list, loads all data into
919 memory, and calculates the default chunkSize using the following method
920 as shown in its code:
921
922 chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
923 if extra: chunkSize += 1
924
925 For example, the default chunkSize will be 7 for a pool of 4 worker processes
926 and 100 data items.
927
928 The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
929 'lazy' RDKit data iterable to retrieve data as needed, without loading all the
930 data into memory. Consequently, the size of input data is not known a priori.
931 It's not possible to estimate an optimal value for the chunkSize. The default
932 chunkSize is set to 1.
933
934 The default value for the chunkSize during 'Lazy' data mode may adversely
935 impact the performance due to the overhead associated with exchanging
936 small chunks of data. It is generally a good idea to explicitly set chunkSize to
937 a larger value during 'Lazy' input data mode, based on the size of your input
938 data and number of processes in the process pool.
939
940 The mp.Pool.map() function waits for all worker processes to process all
941 the data and return the results. The mp.Pool.imap() function, however,
942 returns the the results obtained from worker processes as soon as the
943 results become available for specified chunks of data.
944
945 The order of data in the results returned by both mp.Pool.map() and
946 mp.Pool.imap() functions always corresponds to the input data.
947 -o, --outfile <outfile>
948 Output file name.
949 --outfileParams <Name,Value,...> [default: auto]
950 A comma delimited list of parameter name and value pairs for writing
951 molecules to files. The supported parameter names for different file
952 formats, along with their default values, are shown below:
953
954 SD: kekulize,yes,forceV3000,no
955
956 --overwrite
957 Overwrite existing files.
958 -q, --quiet <yes or no> [default: no]
959 Use quiet mode. The warning and information messages will not be printed.
960 -r, --removeHydrogens <yes or no> [default: Yes]
961 Remove hydrogens after minimization.
962 --randomSeed <number> [default: auto]
963 Seed for the random number generator for reproducing 3D coordinates.
964 Default is to use a random seed.
965 -w, --workingdir <dir>
966 Location of working directory which defaults to the current directory.
967
968 Examples:
969 To generate conformers using Experimental Torsion-angle preference along
970 with basic Knowledge-terms and Distance Geometry (ETKDG) followed by
971 MMFF minimization with automatic determination of maximum number of
972 conformers for each molecule and write out a SD file, type:
973
974 % RDKitGenerateConformers.py -i Sample.smi -o SampleOut.sdf
975
976 To rerun the first example in a quiet mode and write out a SD file, type:
977
978 % RDKitGenerateConformers.py -q yes -i Sample.smi -o SampleOut.sdf
979
980 To rerun the first example in multiprocessing mode on all available CPUs
981 without loading all data into memory and write out a SD file, type:
982
983 % RDKitGenerateConformers.py --mp yes -i Sample.smi -o SampleOut.sdf
984
985 To run the first example in multiprocessing mode on all available CPUs
986 by loading all data into memory and write out a SD file, type:
987
988 % RDKitGenerateConformers.py --mp yes --mpParams "inputDataMode,
989 InMemory" -i Sample.smi -o SampleOut.sdf
990
991 To rerun the first example in multiprocessing mode on specific number of
992 CPUs and chunk size without loading all data into memory and write out a SD file,
993 type:
994
995 % RDKitGenerateConformers.py --mp yes --mpParams "inputDataMode,Lazy,
996 numProcesses,4,chunkSize,8" -i Sample.smi -o SampleOut.sdf
997
998 To generate up to 150 conformers for each molecule using ETKDG and UFF forcefield
999 minimization along with conformers within 25 kcal/mol energy window and write out a
1000 SD file, type:
1001
1002 % RDKitGenerateConformers.py --energyWindow 25 -f UFF --maxConfs 150
1003 -i Sample.smi -o SampleOut.sdf
1004
1005 To generate up to 50 conformers for each molecule using KDG without any forcefield
1006 minimization and alignment of conformers and write out a SD file, type:
1007
1008 % RDKitGenerateConformers.py -f none --maxConfs 50 --alignConformers no
1009 -i Sample.sdf -o SampleOut.sdf
1010
1011 To generate up to 50 conformers using SDG without any forcefield minimization
1012 and alignment of conformers for molecules in a CSV SMILES file, SMILES strings
1013 in column 1, name in column 2, and write out a SD file, type:
1014
1015 % RDKitGenerateConformers.py --maxConfs 50 --maxIters 50 -c SDG
1016 --alignConformers no -f none --infileParams "smilesDelimiter,comma,
1017 smilesTitleLine,yes, smilesColumn,1,smilesNameColumn,2"
1018 -i SampleSMILES.csv -o SampleOut.sdf
1019
1020 Author:
1021 Manish Sud(msud@san.rr.com)
1022
1023 See also:
1024 RDKitCalculateRMSD.py, RDKitCalculateMolecularDescriptors.py,
1025 RDKitCompareMoleculeShapes.py, RDKitConvertFileFormat.py,
1026 RDKitGenerateConstrainedConformers.py, RDKitPerformMinimization.py
1027
1028 Copyright:
1029 Copyright (C) 2026 Manish Sud. All rights reserved.
1030
1031 The functionality available in this script is implemented using RDKit, an
1032 open source toolkit for cheminformatics developed by Greg Landrum.
1033
1034 This file is part of MayaChemTools.
1035
1036 MayaChemTools is free software; you can redistribute it and/or modify it under
1037 the terms of the GNU Lesser General Public License as published by the Free
1038 Software Foundation; either version 3 of the License, or (at your option) any
1039 later version.
1040
1041 """
1042
1043 if __name__ == "__main__":
1044 main()