1 #!/bin/env python
2 #
3 # File: RDKitGenerateConstrainedConformers.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 rdFMCS
43 from rdkit.Chem import rdMolAlign
44 except ImportError as ErrMsg:
45 sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg)
46 sys.stderr.write("Check/update your RDKit environment and try again.\n\n")
47 sys.exit(1)
48
49 # MayaChemTools imports...
50 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
51 try:
52 from docopt import docopt
53 import MiscUtil
54 import RDKitUtil
55 except ImportError as ErrMsg:
56 sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
57 sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
58 sys.exit(1)
59
60 ScriptName = os.path.basename(sys.argv[0])
61 Options = {}
62 OptionsInfo = {}
63
64
65 def main():
66 """Start execution of the script."""
67
68 MiscUtil.PrintInfo(
69 "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n"
70 % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime())
71 )
72
73 (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
74
75 # Retrieve command line arguments and options...
76 RetrieveOptions()
77
78 # Process and validate command line arguments and options...
79 ProcessOptions()
80
81 # Perform actions required by the script...
82 GenerateConstrainedConformers()
83
84 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
85 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
86
87
88 def GenerateConstrainedConformers():
89 """Generate constrained conformers."""
90
91 # Read and validate reference molecule...
92 RefMol = RetrieveReferenceMolecule()
93
94 # Setup a molecule reader for input file...
95 MiscUtil.PrintInfo("\nProcessing file %s..." % OptionsInfo["Infile"])
96 OptionsInfo["InfileParams"]["AllowEmptyMols"] = True
97 Mols = RDKitUtil.ReadMolecules(OptionsInfo["Infile"], **OptionsInfo["InfileParams"])
98
99 # Set up a molecule writer...
100 Writer = RDKitUtil.MoleculesWriter(OptionsInfo["Outfile"], **OptionsInfo["OutfileParams"])
101 if Writer is None:
102 MiscUtil.PrintError("Failed to setup a writer for output fie %s " % OptionsInfo["Outfile"])
103 MiscUtil.PrintInfo("Generating file %s..." % OptionsInfo["Outfile"])
104
105 MolCount, ValidMolCount, CoreScaffoldMissingCount, ConfGenFailedCount = ProcessMolecules(RefMol, Mols, Writer)
106
107 if Writer is not None:
108 Writer.close()
109
110 MiscUtil.PrintInfo("\nTotal number of molecules: %d" % MolCount)
111 MiscUtil.PrintInfo("Number of valid molecules: %d" % ValidMolCount)
112 MiscUtil.PrintInfo("Number of molecules with missing core scaffold: %d" % CoreScaffoldMissingCount)
113 MiscUtil.PrintInfo(
114 "Number of molecules failed during conformation generation or minimization: %d" % ConfGenFailedCount
115 )
116 MiscUtil.PrintInfo(
117 "Number of ignored molecules: %d" % (MolCount - ValidMolCount + CoreScaffoldMissingCount + ConfGenFailedCount)
118 )
119
120
121 def ProcessMolecules(RefMol, Mols, Writer):
122 """Process molecules to generate constrained conformers."""
123
124 if OptionsInfo["MPMode"]:
125 return ProcessMoleculesUsingMultipleProcesses(RefMol, Mols, Writer)
126 else:
127 return ProcessMoleculesUsingSingleProcess(RefMol, Mols, Writer)
128
129
130 def ProcessMoleculesUsingSingleProcess(RefMol, Mols, Writer):
131 """Process molecules to generate constrained conformers using a single process."""
132
133 (MolCount, ValidMolCount, CoreScaffoldMissingCount, ConfGenFailedCount) = [0] * 4
134
135 for Mol in Mols:
136 MolCount += 1
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 # Setup a reference molecule core containing common scaffold atoms...
149 RefMolCore = SetupCoreScaffold(RefMol, Mol, MolCount)
150 if RefMolCore is None:
151 CoreScaffoldMissingCount += 1
152 continue
153
154 ConfMols, CalcStatus, ConfIDs, ConfEnergyValues, ConfScaffoldEmbedRMSDValues = GenerateMolConformers(
155 Mol, RefMolCore, MolCount
156 )
157
158 if not CalcStatus:
159 ConfGenFailedCount += 1
160 continue
161
162 WriteMolConformers(Writer, Mol, MolCount, ConfMols, ConfIDs, ConfEnergyValues, ConfScaffoldEmbedRMSDValues)
163
164 return (MolCount, ValidMolCount, CoreScaffoldMissingCount, ConfGenFailedCount)
165
166
167 def ProcessMoleculesUsingMultipleProcesses(RefMol, Mols, Writer):
168 """Process molecules to generate constrained conformers using multiprocessing."""
169
170 MPParams = OptionsInfo["MPParams"]
171
172 # Setup data for initializing a worker process...
173 MiscUtil.PrintInfo("Encoding options info and reference molecule...")
174
175 OptionsInfo["EncodedRefMol"] = RDKitUtil.MolToBase64EncodedMolString(RefMol)
176 InitializeWorkerProcessArgs = (
177 MiscUtil.ObjectToBase64EncodedString(Options),
178 MiscUtil.ObjectToBase64EncodedString(OptionsInfo),
179 )
180
181 # Setup a encoded mols data iterable for a worker process...
182 WorkerProcessDataIterable = RDKitUtil.GenerateBase64EncodedMolStrings(Mols)
183
184 # Setup process pool along with data initialization for each process...
185 MiscUtil.PrintInfo(
186 "\nConfiguring multiprocessing using %s method..."
187 % ("mp.Pool.imap()" if re.match("^Lazy$", MPParams["InputDataMode"], re.I) else "mp.Pool.map()")
188 )
189 MiscUtil.PrintInfo(
190 "NumProcesses: %s; InputDataMode: %s; ChunkSize: %s\n"
191 % (
192 MPParams["NumProcesses"],
193 MPParams["InputDataMode"],
194 ("automatic" if MPParams["ChunkSize"] is None else MPParams["ChunkSize"]),
195 )
196 )
197
198 ProcessPool = mp.Pool(MPParams["NumProcesses"], InitializeWorkerProcess, InitializeWorkerProcessArgs)
199
200 # Start processing...
201 if re.match("^Lazy$", MPParams["InputDataMode"], re.I):
202 Results = ProcessPool.imap(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
203 elif re.match("^InMemory$", MPParams["InputDataMode"], re.I):
204 Results = ProcessPool.map(WorkerProcess, WorkerProcessDataIterable, MPParams["ChunkSize"])
205 else:
206 MiscUtil.PrintError(
207 'The value, %s, specified for "--inputDataMode" is not supported.' % (MPParams["InputDataMode"])
208 )
209
210 (MolCount, ValidMolCount, CoreScaffoldMissingCount, ConfGenFailedCount) = [0] * 4
211 for Result in Results:
212 MolCount += 1
213 (
214 MolIndex,
215 EncodedMol,
216 EncodedConfMols,
217 CoreScaffoldMissingStatus,
218 CalcStatus,
219 ConfIDs,
220 ConfEnergyValues,
221 ConfScaffoldEmbedRMSDValues,
222 ) = Result
223
224 if EncodedMol is None:
225 continue
226 ValidMolCount += 1
227
228 if CoreScaffoldMissingStatus:
229 CoreScaffoldMissingCount += 1
230 continue
231
232 if not CalcStatus:
233 ConfGenFailedCount += 1
234 continue
235
236 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
237 ConfMols = [RDKitUtil.MolFromBase64EncodedMolString(EncodedConfMol) for EncodedConfMol in EncodedConfMols]
238
239 WriteMolConformers(Writer, Mol, MolCount, ConfMols, ConfIDs, ConfEnergyValues, ConfScaffoldEmbedRMSDValues)
240
241 return (MolCount, ValidMolCount, CoreScaffoldMissingCount, ConfGenFailedCount)
242
243
244 def InitializeWorkerProcess(*EncodedArgs):
245 """Initialize data for a worker process."""
246
247 global Options, OptionsInfo
248
249 MiscUtil.PrintInfo("Starting process (PID: %s)..." % os.getpid())
250
251 # Decode Options and OptionInfo...
252 Options = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[0])
253 OptionsInfo = MiscUtil.ObjectFromBase64EncodedString(EncodedArgs[1])
254
255 # Decode RefMol...
256 OptionsInfo["RefMol"] = RDKitUtil.MolFromBase64EncodedMolString(OptionsInfo["EncodedRefMol"])
257
258
259 def WorkerProcess(EncodedMolInfo):
260 """Process data for a worker process."""
261
262 MolIndex, EncodedMol = EncodedMolInfo
263
264 ConfMols = None
265 CoreScaffoldMissingStatus = False
266 CalcStatus = False
267 ConfIDs = None
268 ConfEnergyValues = None
269 ConfScaffoldEmbedRMSDValues = None
270
271 if EncodedMol is None:
272 return [
273 MolIndex,
274 None,
275 ConfMols,
276 CoreScaffoldMissingStatus,
277 CalcStatus,
278 ConfIDs,
279 ConfEnergyValues,
280 ConfScaffoldEmbedRMSDValues,
281 ]
282
283 RefMol = OptionsInfo["RefMol"]
284
285 Mol = RDKitUtil.MolFromBase64EncodedMolString(EncodedMol)
286 if RDKitUtil.IsMolEmpty(Mol):
287 if not OptionsInfo["QuietMode"]:
288 MolName = RDKitUtil.GetMolName(Mol, (MolIndex + 1))
289 MiscUtil.PrintWarning("Ignoring empty molecule: %s" % MolName)
290 return [
291 MolIndex,
292 None,
293 ConfMols,
294 CoreScaffoldMissingStatus,
295 CalcStatus,
296 ConfIDs,
297 ConfEnergyValues,
298 ConfScaffoldEmbedRMSDValues,
299 ]
300
301 # Setup a reference molecule core containing common scaffold atoms...
302 RefMolCore = SetupCoreScaffold(RefMol, Mol, (MolIndex + 1))
303 if RefMolCore is None:
304 CoreScaffoldMissingStatus = True
305 return [
306 MolIndex,
307 EncodedMol,
308 ConfMols,
309 CoreScaffoldMissingStatus,
310 CalcStatus,
311 ConfIDs,
312 ConfEnergyValues,
313 ConfScaffoldEmbedRMSDValues,
314 ]
315
316 ConfMols, CalcStatus, ConfIDs, ConfEnergyValues, ConfScaffoldEmbedRMSDValues = GenerateMolConformers(
317 Mol, RefMolCore, (MolIndex + 1)
318 )
319
320 EncodedConfMols = None
321 if ConfMols is not None:
322 EncodedConfMols = [
323 RDKitUtil.MolToBase64EncodedMolString(
324 ConfMol,
325 PropertyPickleFlags=Chem.PropertyPickleOptions.MolProps | Chem.PropertyPickleOptions.PrivateProps,
326 )
327 for ConfMol in ConfMols
328 ]
329
330 return [
331 MolIndex,
332 EncodedMol,
333 EncodedConfMols,
334 CoreScaffoldMissingStatus,
335 CalcStatus,
336 ConfIDs,
337 ConfEnergyValues,
338 ConfScaffoldEmbedRMSDValues,
339 ]
340
341
342 def RetrieveReferenceMolecule():
343 """Retrieve and validate reference molecule."""
344
345 RefFile = OptionsInfo["RefFile"]
346
347 MiscUtil.PrintInfo("\nProcessing file %s..." % (RefFile))
348 OptionsInfo["InfileParams"]["AllowEmptyMols"] = False
349 ValidRefMols, RefMolCount, ValidRefMolCount = RDKitUtil.ReadAndValidateMolecules(
350 RefFile, **OptionsInfo["InfileParams"]
351 )
352
353 if ValidRefMolCount == 0:
354 MiscUtil.PrintError("The reference file, %s, contains no valid molecules." % RefFile)
355 elif ValidRefMolCount > 1:
356 MiscUtil.PrintWarning(
357 "The reference file, %s, contains, %d, valid molecules. Using first molecule as the reference molecule..."
358 % (RefFile, ValidRefMolCount)
359 )
360
361 RefMol = ValidRefMols[0]
362
363 if OptionsInfo["UseScaffoldSMARTS"]:
364 ScaffoldPatternMol = Chem.MolFromSmarts(OptionsInfo["ScaffoldSMARTS"])
365 if ScaffoldPatternMol is None:
366 MiscUtil.PrintError(
367 'Failed to create scaffold pattern molecule. The scaffold SMARTS pattern, %s, specified using "-s, --scaffold" option is not valid.'
368 % (OptionsInfo["ScaffoldSMARTS"])
369 )
370
371 if not RefMol.HasSubstructMatch(ScaffoldPatternMol):
372 MiscUtil.PrintError(
373 'The scaffold SMARTS pattern, %s, specified using "-s, --scaffold" option, is missing in the first valid reference molecule.'
374 % (OptionsInfo["ScaffoldSMARTS"])
375 )
376
377 return RefMol
378
379
380 def SetupCoreScaffold(RefMol, Mol, MolCount):
381 """Setup a reference molecule core containing common scaffold atoms between
382 a pair of molecules."""
383
384 if OptionsInfo["UseScaffoldMCS"]:
385 return SetupCoreScaffoldByMCS(RefMol, Mol, MolCount)
386 elif OptionsInfo["UseScaffoldSMARTS"]:
387 return SetupCoreScaffoldBySMARTS(RefMol, Mol, MolCount)
388 else:
389 MiscUtil.PrintError(
390 'The value, %s, specified for "-s, --scaffold" option is not supported.' % (OptionsInfo["Scaffold"])
391 )
392
393
394 def SetupCoreScaffoldByMCS(RefMol, Mol, MolCount):
395 """Setup a reference molecule core containing common scaffold atoms between
396 a pair of molecules using MCS."""
397
398 MCSParams = OptionsInfo["MCSParams"]
399 Mols = [RefMol, Mol]
400
401 MCSResultObject = rdFMCS.FindMCS(
402 Mols,
403 maximizeBonds=MCSParams["MaximizeBonds"],
404 threshold=MCSParams["Threshold"],
405 timeout=MCSParams["TimeOut"],
406 verbose=MCSParams["Verbose"],
407 matchValences=MCSParams["MatchValences"],
408 ringMatchesRingOnly=MCSParams["RingMatchesRingOnly"],
409 completeRingsOnly=MCSParams["CompleteRingsOnly"],
410 matchChiralTag=MCSParams["MatchChiralTag"],
411 atomCompare=MCSParams["AtomCompare"],
412 bondCompare=MCSParams["BondCompare"],
413 seedSmarts=MCSParams["SeedSMARTS"],
414 )
415
416 if MCSResultObject.canceled:
417 if not OptionsInfo["QuietMode"]:
418 MiscUtil.PrintWarning(
419 'MCS failed to identify a common core scaffold between reference moecule and input molecule %s. Specify a different set of parameters using "-m, --mcsParams" option and try again.'
420 % (RDKitUtil.GetMolName(Mol, MolCount))
421 )
422 return None
423
424 CoreNumAtoms = MCSResultObject.numAtoms
425 CoreNumBonds = MCSResultObject.numBonds
426
427 SMARTSCore = MCSResultObject.smartsString
428
429 if not len(SMARTSCore):
430 if not OptionsInfo["QuietMode"]:
431 MiscUtil.PrintWarning(
432 'MCS failed to identify a common core scaffold between reference moecule and input molecule %s. Specify a different set of parameters using "-m, --mcsParams" option and try again.'
433 % (RDKitUtil.GetMolName(Mol, MolCount))
434 )
435 return None
436
437 if CoreNumAtoms < MCSParams["MinNumAtoms"]:
438 if not OptionsInfo["QuietMode"]:
439 MiscUtil.PrintWarning(
440 'Number of atoms, %d, in core scaffold identified by MCS is less than, %d, as specified by "minNumAtoms" parameter in "-m, --mcsParams" option.'
441 % (CoreNumAtoms, MCSParams["MinNumAtoms"])
442 )
443 return None
444
445 if CoreNumBonds < MCSParams["MinNumBonds"]:
446 if not OptionsInfo["QuietMode"]:
447 MiscUtil.PrintWarning(
448 'Number of bonds, %d, in core scaffold identified by MCS is less than, %d, as specified by "minNumBonds" parameter in "-m, --mcsParams" option.'
449 % (CoreNumBonds, MCSParams["MinNumBonds"])
450 )
451 return None
452
453 return GenerateCoreMol(RefMol, SMARTSCore)
454
455
456 def SetupCoreScaffoldBySMARTS(RefMol, Mol, MolCount):
457 """Setup a reference molecule core containing common scaffold atoms between
458 a pair of molecules using specified SMARTS."""
459
460 if OptionsInfo["ScaffoldPatternMol"] is None:
461 OptionsInfo["ScaffoldPatternMol"] = Chem.MolFromSmarts(OptionsInfo["ScaffoldSMARTS"])
462
463 if not Mol.HasSubstructMatch(OptionsInfo["ScaffoldPatternMol"]):
464 if not OptionsInfo["QuietMode"]:
465 MiscUtil.PrintWarning(
466 'The scaffold SMARTS pattern, %s, specified using "-s, --scaffold" option is missing in input molecule, %s.'
467 % (OptionsInfo["ScaffoldSMARTS"], RDKitUtil.GetMolName(Mol, MolCount))
468 )
469 return None
470
471 return GenerateCoreMol(RefMol, OptionsInfo["ScaffoldSMARTS"])
472
473
474 def GenerateCoreMol(RefMol, SMARTSCore):
475 """Generate core molecule for embedding."""
476
477 # Create a molecule corresponding to core atoms...
478 SMARTSCoreMol = Chem.MolFromSmarts(SMARTSCore)
479
480 # Setup a ref molecule containing core atoms with dummy atoms as
481 # attachment points for atoms around the core atoms...
482 Core = AllChem.ReplaceSidechains(Chem.RemoveHs(RefMol), SMARTSCoreMol)
483
484 # Delete any substructures containing dummy atoms..
485 RefMolCore = AllChem.DeleteSubstructs(Core, Chem.MolFromSmiles("*"))
486 RefMolCore.UpdatePropertyCache()
487
488 return RefMolCore
489
490
491 def GenerateMolConformers(Mol, RefMolCore, MolNum=None):
492 """Generate constrained conformers."""
493
494 if OptionsInfo["AddHydrogens"]:
495 Mol = Chem.AddHs(Mol, addCoords=True)
496
497 # Setup forcefield function to use for constrained minimization...
498 ForceFieldFunction = None
499 ForceFieldName = None
500 if OptionsInfo["UseUFF"]:
501 ForceFieldFunction = lambda mol, confId=-1: AllChem.UFFGetMoleculeForceField(mol, confId=confId)
502 ForceFieldName = "UFF"
503 else:
504 ForceFieldFunction = lambda mol, confId=-1: AllChem.MMFFGetMoleculeForceField(
505 mol, AllChem.MMFFGetMoleculeProperties(mol, mmffVariant=OptionsInfo["MMFFVariant"]), confId=confId
506 )
507 ForceFieldName = "MMFF"
508
509 if ForceFieldFunction is None:
510 if not OptionsInfo["QuietMode"]:
511 MiscUtil.PrintWarning(
512 "Failed to setup forcefield %s for molecule: %s\n" % (ForceFieldName, RDKitUtil.GetMolName(Mol, MolNum))
513 )
514 return (None, False, None, None, None)
515
516 MaxConfs = OptionsInfo["MaxConfs"]
517 EnforceChirality = OptionsInfo["EnforceChirality"]
518 UseExpTorsionAnglePrefs = OptionsInfo["UseExpTorsionAnglePrefs"]
519 ETVersion = OptionsInfo["ETVersion"]
520 UseBasicKnowledge = OptionsInfo["UseBasicKnowledge"]
521 UseTethers = OptionsInfo["UseTethers"]
522
523 CalcEnergyMap = {}
524 MolConfsMap = {}
525 CalcRMSDMap = {}
526
527 ScaffoldEmbedRMSDMap = {}
528
529 ConfIDs = [ConfID for ConfID in range(0, MaxConfs)]
530 for ConfID in ConfIDs:
531 try:
532 MolConf = Chem.Mol(Mol)
533 AllChem.ConstrainedEmbed(
534 MolConf,
535 RefMolCore,
536 useTethers=UseTethers,
537 coreConfId=-1,
538 randomseed=ConfID,
539 getForceField=ForceFieldFunction,
540 enforceChirality=EnforceChirality,
541 useExpTorsionAnglePrefs=UseExpTorsionAnglePrefs,
542 useBasicKnowledge=UseBasicKnowledge,
543 ETversion=ETVersion,
544 )
545 except (ValueError, RuntimeError, Chem.rdchem.KekulizeException) as ErrMsg:
546 if not OptionsInfo["QuietMode"]:
547 MolName = RDKitUtil.GetMolName(Mol, MolNum)
548 MiscUtil.PrintWarning(
549 "Constrained embedding coupldn't be performed for molecule %s:\n%s\n"
550 % (RDKitUtil.GetMolName(Mol, MolNum), ErrMsg)
551 )
552 return (None, False, None, None, None)
553
554 EnergyStatus, Energy = GetEnergy(MolConf)
555
556 if not EnergyStatus:
557 if not OptionsInfo["QuietMode"]:
558 MolName = RDKitUtil.GetMolName(Mol, MolNum)
559 MiscUtil.PrintWarning(
560 "Failed to retrieve calculated energy for conformation number %d of molecule %s. Try again after removing any salts or cleaing up the molecule...\n"
561 % (ConfID, MolName)
562 )
563 return (None, False, None, None, None)
564
565 CalcEnergyMap[ConfID] = Energy
566
567 if OptionsInfo["ScaffoldRMSDOut"]:
568 ScaffoldEmbedRMSDMap[ConfID] = "%.4f" % float(MolConf.GetProp("EmbedRMS"))
569 MolConf.ClearProp("EmbedRMS")
570
571 if OptionsInfo["RemoveHydrogens"]:
572 MolConf = Chem.RemoveHs(MolConf)
573 MolConfsMap[ConfID] = MolConf
574
575 # Sort conformers by energy...
576 SortedConfIDs = sorted(ConfIDs, key=lambda ConfID: CalcEnergyMap[ConfID])
577
578 MinEnergyConfID = SortedConfIDs[0]
579 EnergyWindow = OptionsInfo["EnergyWindow"]
580
581 MinConfEnergy = CalcEnergyMap[MinEnergyConfID]
582 MinEnergyMolConf = MolConfsMap[MinEnergyConfID]
583
584 # Calculate RMSD values for conformers...
585 CalcRMSDMap = {}
586
587 EnergyRMSDCutoff = OptionsInfo["EnergyRMSDCutoff"]
588 ApplyEnergyRMSDCutoff = True if EnergyRMSDCutoff > 0 else False
589
590 if ApplyEnergyRMSDCutoff:
591 FirstConf = True
592 for ConfID in SortedConfIDs:
593 if FirstConf:
594 FirstConf = False
595 CalcRMSDMap[ConfID] = 0.0
596 continue
597
598 # Make a copy for probe molecule. It gets updated during the calculation...
599 ProbeMolConf = Chem.Mol(MolConfsMap[ConfID])
600 RMSD = rdMolAlign.AlignMol(ProbeMolConf, MinEnergyMolConf)
601 CalcRMSDMap[ConfID] = RMSD
602
603 # Track conformers with in the specified energy window from the lowest
604 # energy conformation along with applying RMSD cutoff as needed...
605 #
606 SelectedConfIDs = []
607
608 ConfCount = 0
609 IgnoredByEnergyConfCount = 0
610 IgnoredByRMSDConfCount = 0
611
612 FirstConf = True
613 for ConfID in SortedConfIDs:
614 if FirstConf:
615 ConfCount += 1
616 FirstConf = False
617 SelectedConfIDs.append(ConfID)
618 continue
619
620 ConfEnergyDiff = abs(CalcEnergyMap[ConfID] - MinConfEnergy)
621 if ConfEnergyDiff > EnergyWindow:
622 IgnoredByEnergyConfCount += 1
623 continue
624
625 if ApplyEnergyRMSDCutoff:
626 if CalcRMSDMap[ConfID] < EnergyRMSDCutoff:
627 IgnoredByRMSDConfCount += 1
628 continue
629
630 ConfCount += 1
631 SelectedConfIDs.append(ConfID)
632
633 if not OptionsInfo["QuietMode"]:
634 MolName = RDKitUtil.GetMolName(Mol, MolNum)
635 MiscUtil.PrintInfo("\nTotal Number of conformations generated for %s: %d" % (MolName, ConfCount))
636 MiscUtil.PrintInfo(
637 "Number of conformations ignored due to energy window cutoff: %d" % (IgnoredByEnergyConfCount)
638 )
639 if ApplyEnergyRMSDCutoff:
640 MiscUtil.PrintInfo(
641 "Number of conformations ignored due to energy RMSD cutoff: %d" % (IgnoredByRMSDConfCount)
642 )
643
644 # Setup selected conformer molecules...
645 SelectedConfMols = [MolConfsMap[ConfID] for ConfID in SelectedConfIDs]
646
647 # Setup selected conformer energy values...
648 SelectedConfEnergyValues = None
649 if OptionsInfo["EnergyOut"]:
650 SelectedConfEnergyValues = ["%.2f" % CalcEnergyMap[ConfID] for ConfID in SelectedConfIDs]
651
652 # Setup selected conformer scaffold RMSD values...
653 SelectedConfScaffoldEmbedRMSDValues = None
654 if OptionsInfo["ScaffoldRMSDOut"]:
655 SelectedConfScaffoldEmbedRMSDValues = []
656 for ConfID in SelectedConfIDs:
657 SelectedConfScaffoldEmbedRMSDValues.append(ScaffoldEmbedRMSDMap[ConfID])
658
659 return (SelectedConfMols, True, SelectedConfIDs, SelectedConfEnergyValues, SelectedConfScaffoldEmbedRMSDValues)
660
661
662 def GetEnergy(Mol, ConfID=None):
663 """Calculate energy."""
664
665 Status = True
666 Energy = None
667
668 if ConfID is None:
669 ConfID = -1
670
671 if OptionsInfo["UseUFF"]:
672 UFFMoleculeForcefield = AllChem.UFFGetMoleculeForceField(Mol, confId=ConfID)
673 if UFFMoleculeForcefield is None:
674 Status = False
675 else:
676 Energy = UFFMoleculeForcefield.CalcEnergy()
677 elif OptionsInfo["UseMMFF"]:
678 MMFFMoleculeProperties = AllChem.MMFFGetMoleculeProperties(Mol, mmffVariant=OptionsInfo["MMFFVariant"])
679 MMFFMoleculeForcefield = AllChem.MMFFGetMoleculeForceField(Mol, MMFFMoleculeProperties, confId=ConfID)
680 if MMFFMoleculeForcefield is None:
681 Status = False
682 else:
683 Energy = MMFFMoleculeForcefield.CalcEnergy()
684 else:
685 MiscUtil.PrintError(
686 "Couldn't retrieve conformer energy: Specified forcefield, %s, is not supported" % OptionsInfo["ForceField"]
687 )
688
689 return (Status, Energy)
690
691
692 def WriteMolConformers(Writer, Mol, MolNum, ConfMols, ConfIDs, ConfEnergyValues=None, ConfScaffoldEmbedRMSDValues=None):
693 """Write molecule coformers."""
694
695 if ConfMols is None:
696 return
697
698 for Index, ConfMol in enumerate(ConfMols):
699 ConfMolName = RDKitUtil.GetMolName(Mol, MolNum)
700 SetConfMolName(ConfMol, ConfMolName, ConfIDs[Index])
701
702 if ConfScaffoldEmbedRMSDValues is not None:
703 ConfMol.SetProp("CoreScaffoldEmbedRMSD", ConfScaffoldEmbedRMSDValues[Index])
704
705 if ConfEnergyValues is not None:
706 ConfMol.SetProp(OptionsInfo["EnergyLabel"], ConfEnergyValues[Index])
707
708 Writer.write(ConfMol)
709
710
711 def SetConfMolName(Mol, MolName, ConfCount):
712 """Set conf mol name."""
713
714 ConfName = "%s_Conf%d" % (MolName, ConfCount)
715 Mol.SetProp("_Name", ConfName)
716
717
718 def ProcessMCSParameters():
719 """Set up and process MCS parameters."""
720
721 SetupMCSParameters()
722 ProcessSpecifiedMCSParameters()
723
724
725 def SetupMCSParameters():
726 """Set up default MCS parameters."""
727
728 OptionsInfo["MCSParams"] = {
729 "MaximizeBonds": True,
730 "Threshold": 0.9,
731 "TimeOut": 3600,
732 "Verbose": False,
733 "MatchValences": True,
734 "MatchChiralTag": False,
735 "RingMatchesRingOnly": True,
736 "CompleteRingsOnly": True,
737 "AtomCompare": rdFMCS.AtomCompare.CompareElements,
738 "BondCompare": rdFMCS.BondCompare.CompareOrder,
739 "SeedSMARTS": "",
740 "MinNumAtoms": 1,
741 "MinNumBonds": 0,
742 }
743
744
745 def ProcessSpecifiedMCSParameters():
746 """Process specified MCS parameters."""
747
748 if re.match("^auto$", OptionsInfo["SpecifiedMCSParams"], re.I):
749 # Nothing to process...
750 return
751
752 # Parse specified parameters...
753 MCSParams = re.sub(" ", "", OptionsInfo["SpecifiedMCSParams"])
754 if not MCSParams:
755 MiscUtil.PrintError('No valid parameter name and value pairs specified using "-m, --mcsParams" option.')
756
757 MCSParamsWords = MCSParams.split(",")
758 if len(MCSParamsWords) % 2:
759 MiscUtil.PrintError(
760 'The number of comma delimited paramater names and values, %d, specified using "-m, --mcsParams" option must be an even number.'
761 % (len(MCSParamsWords))
762 )
763
764 # Setup canonical parameter names...
765 ValidParamNames = []
766 CanonicalParamNamesMap = {}
767 for ParamName in sorted(OptionsInfo["MCSParams"]):
768 ValidParamNames.append(ParamName)
769 CanonicalParamNamesMap[ParamName.lower()] = ParamName
770
771 # Validate and set paramater names and value...
772 for Index in range(0, len(MCSParamsWords), 2):
773 Name = MCSParamsWords[Index]
774 Value = MCSParamsWords[Index + 1]
775
776 CanonicalName = Name.lower()
777 if CanonicalName not in CanonicalParamNamesMap:
778 MiscUtil.PrintError(
779 'The parameter name, %s, specified using "-m, --mcsParams" option is not a valid name. Supported parameter names: %s'
780 % (Name, " ".join(ValidParamNames))
781 )
782
783 ParamName = CanonicalParamNamesMap[CanonicalName]
784 if re.match("^Threshold$", ParamName, re.I):
785 Value = float(Value)
786 if Value <= 0.0 or Value > 1.0:
787 MiscUtil.PrintError(
788 'The parameter value, %s, specified using "-m, --mcsParams" option for parameter, %s, is not a valid value. Supported values: > 0 and <= 1.0'
789 % (Value, Name)
790 )
791 ParamValue = Value
792 elif re.match("^Timeout$", ParamName, re.I):
793 Value = int(Value)
794 if Value <= 0:
795 MiscUtil.PrintError(
796 'The parameter value, %s, specified using "-m, --mcsParams" option for parameter, %s, is not a valid value. Supported values: > 0'
797 % (Value, Name)
798 )
799 ParamValue = Value
800 elif re.match("^MinNumAtoms$", ParamName, re.I):
801 Value = int(Value)
802 if Value < 1:
803 MiscUtil.PrintError(
804 'The parameter value, %s, specified using "-m, --mcsParams" option for parameter, %s, is not a valid value. Supported values: >= 1'
805 % (Value, Name)
806 )
807 ParamValue = Value
808 elif re.match("^MinNumBonds$", ParamName, re.I):
809 Value = int(Value)
810 if Value < 0:
811 MiscUtil.PrintError(
812 'The parameter value, %s, specified using "-m, --mcsParams" option for parameter, %s, is not a valid value. Supported values: >=0 '
813 % (Value, Name)
814 )
815 ParamValue = Value
816 elif re.match("^AtomCompare$", ParamName, re.I):
817 if re.match("^CompareAny$", Value, re.I):
818 ParamValue = rdFMCS.AtomCompare.CompareAny
819 elif re.match("^CompareElements$", Value, re.I):
820 ParamValue = Chem.rdFMCS.AtomCompare.CompareElements
821 elif re.match("^CompareIsotopes$", Value, re.I):
822 ParamValue = Chem.rdFMCS.AtomCompare.CompareIsotopes
823 else:
824 MiscUtil.PrintError(
825 'The parameter value, %s, specified using "-m, --mcsParams" option for parameter, %s, is not a valid value. Supported values: CompareAny CompareElements CompareIsotopes'
826 % (Value, Name)
827 )
828 elif re.match("^BondCompare$", ParamName, re.I):
829 if re.match("^CompareAny$", Value, re.I):
830 ParamValue = Chem.rdFMCS.BondCompare.CompareAny
831 elif re.match("^CompareOrder$", Value, re.I):
832 ParamValue = rdFMCS.BondCompare.CompareOrder
833 elif re.match("^CompareOrderExact$", Value, re.I):
834 ParamValue = rdFMCS.BondCompare.CompareOrderExact
835 else:
836 MiscUtil.PrintError(
837 'The parameter value, %s, specified using "-m, --mcsParams" option for parameter, %s, is not a valid value. Supported values: CompareAny CompareOrder CompareOrderExact'
838 % (Value, Name)
839 )
840 elif re.match("^SeedSMARTS$", ParamName, re.I):
841 if not len(Value):
842 MiscUtil.PrintError(
843 'The parameter value specified using "-m, --mcsParams" option for parameter, %s, is empty. '
844 % (Name)
845 )
846 ParamValue = Value
847 else:
848 if not re.match("^(Yes|No|True|False)$", Value, re.I):
849 MiscUtil.PrintError(
850 'The parameter value, %s, specified using "-m, --mcsParams" option for parameter, %s, is not a valid value. Supported values: Yes No True False'
851 % (Value, Name)
852 )
853 ParamValue = False
854 if re.match("^(Yes|True)$", Value, re.I):
855 ParamValue = True
856
857 # Set value...
858 OptionsInfo["MCSParams"][ParamName] = ParamValue
859
860
861 def ProcesssConformerGeneratorOption():
862 """Process comformer generator option."""
863
864 ConfGenParams = MiscUtil.ProcessOptionConformerGenerator("--conformerGenerator", Options["--conformerGenerator"])
865
866 OptionsInfo["ConformerGenerator"] = ConfGenParams["ConformerGenerator"]
867 OptionsInfo["UseBasicKnowledge"] = ConfGenParams["UseBasicKnowledge"]
868 OptionsInfo["UseExpTorsionAnglePrefs"] = ConfGenParams["UseExpTorsionAnglePrefs"]
869 OptionsInfo["ETVersion"] = ConfGenParams["ETVersion"]
870
871
872 def ProcessOptions():
873 """Process and validate command line arguments and options."""
874
875 MiscUtil.PrintInfo("Processing options...")
876
877 # Validate options...
878 ValidateOptions()
879
880 OptionsInfo["Infile"] = Options["--infile"]
881 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters(
882 "--infileParams", Options["--infileParams"], Options["--infile"]
883 )
884
885 OptionsInfo["RefFile"] = Options["--reffile"]
886
887 OptionsInfo["Scaffold"] = Options["--scaffold"]
888 if re.match("^auto$", Options["--scaffold"], re.I):
889 UseScaffoldMCS = True
890 UseScaffoldSMARTS = False
891 ScaffoldSMARTS = None
892 else:
893 UseScaffoldMCS = False
894 UseScaffoldSMARTS = True
895 ScaffoldSMARTS = OptionsInfo["Scaffold"]
896
897 OptionsInfo["UseScaffoldMCS"] = UseScaffoldMCS
898 OptionsInfo["UseScaffoldSMARTS"] = UseScaffoldSMARTS
899 OptionsInfo["ScaffoldSMARTS"] = ScaffoldSMARTS
900 OptionsInfo["ScaffoldPatternMol"] = None
901
902 OptionsInfo["SpecifiedMCSParams"] = Options["--mcsParams"]
903 ProcessMCSParameters()
904
905 OptionsInfo["Outfile"] = Options["--outfile"]
906 OptionsInfo["OutfileParams"] = MiscUtil.ProcessOptionOutfileParameters(
907 "--outfileParams", Options["--outfileParams"]
908 )
909
910 OptionsInfo["Overwrite"] = Options["--overwrite"]
911
912 OptionsInfo["AddHydrogens"] = True if re.match("^yes$", Options["--addHydrogens"], re.I) else False
913
914 ProcesssConformerGeneratorOption()
915
916 if re.match("^UFF$", Options["--forceField"], re.I):
917 ForceField = "UFF"
918 UseUFF = True
919 UseMMFF = False
920 elif re.match("^MMFF$", Options["--forceField"], re.I):
921 ForceField = "MMFF"
922 UseUFF = False
923 UseMMFF = True
924 else:
925 MiscUtil.PrintError(
926 'The value, %s, specified for "--forceField" is not supported.' % (Options["--forceField"],)
927 )
928
929 MMFFVariant = "MMFF94" if re.match("^MMFF94$", Options["--forceFieldMMFFVariant"], re.I) else "MMFF94s"
930
931 OptionsInfo["ForceField"] = ForceField
932 OptionsInfo["MMFFVariant"] = MMFFVariant
933 OptionsInfo["UseMMFF"] = UseMMFF
934 OptionsInfo["UseUFF"] = UseUFF
935
936 OptionsInfo["ScaffoldRMSDOut"] = True if re.match("^yes$", Options["--scaffoldRMSDOut"], re.I) else False
937
938 OptionsInfo["EnergyOut"] = True if re.match("^yes$", Options["--energyOut"], re.I) else False
939 if UseMMFF:
940 OptionsInfo["EnergyLabel"] = "%s_Energy" % MMFFVariant
941 else:
942 OptionsInfo["EnergyLabel"] = "%s_Energy" % ForceField
943
944 OptionsInfo["EnforceChirality"] = True if re.match("^yes$", Options["--enforceChirality"], re.I) else False
945 EnergyRMSDCutoff = -1.0
946 if not re.match("^none$", Options["--energyRMSDCutoff"], re.I):
947 EnergyRMSDCutoff = float(Options["--energyRMSDCutoff"])
948 OptionsInfo["EnergyRMSDCutoff"] = EnergyRMSDCutoff
949
950 OptionsInfo["EnergyWindow"] = float(Options["--energyWindow"])
951
952 OptionsInfo["MaxConfs"] = int(Options["--maxConfs"])
953
954 OptionsInfo["MPMode"] = True if re.match("^yes$", Options["--mp"], re.I) else False
955 OptionsInfo["MPParams"] = MiscUtil.ProcessOptionMultiprocessingParameters("--mpParams", Options["--mpParams"])
956
957 OptionsInfo["QuietMode"] = True if re.match("^yes$", Options["--quiet"], re.I) else False
958
959 OptionsInfo["RemoveHydrogens"] = True if re.match("^yes$", Options["--removeHydrogens"], re.I) else False
960 OptionsInfo["UseTethers"] = True if re.match("^yes$", Options["--useTethers"], re.I) else False
961
962
963 def RetrieveOptions():
964 """Retrieve command line arguments and options."""
965
966 # Get options...
967 global Options
968 Options = docopt(_docoptUsage_)
969
970 # Set current working directory to the specified directory...
971 WorkingDir = Options["--workingdir"]
972 if WorkingDir:
973 os.chdir(WorkingDir)
974
975 # Handle examples option...
976 if "--examples" in Options and Options["--examples"]:
977 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
978 sys.exit(0)
979
980
981 def ValidateOptions():
982 """Validate option values."""
983
984 MiscUtil.ValidateOptionTextValue("-a, --addHydrogens", Options["--addHydrogens"], "yes no")
985 MiscUtil.ValidateOptionTextValue(
986 "-c, --conformerGenerator", Options["--conformerGenerator"], "SDG KDG ETDG ETKDG ETKDGv2"
987 )
988
989 MiscUtil.ValidateOptionTextValue("-f, --forceField", Options["--forceField"], "UFF MMFF")
990 MiscUtil.ValidateOptionTextValue(" --forceFieldMMFFVariant", Options["--forceFieldMMFFVariant"], "MMFF94 MMFF94s")
991
992 MiscUtil.ValidateOptionFloatValue("--energyWindow", Options["--energyWindow"], {">": 0.0})
993 if not re.match("^none$", Options["--energyRMSDCutoff"], re.I):
994 MiscUtil.ValidateOptionFloatValue("--energyRMSDCutoff", Options["--energyRMSDCutoff"], {">": 0.0})
995
996 MiscUtil.ValidateOptionTextValue("--scaffoldRMSDOut", Options["--scaffoldRMSDOut"], "yes no")
997
998 MiscUtil.ValidateOptionTextValue("--energyOut", Options["--energyOut"], "yes no")
999 MiscUtil.ValidateOptionTextValue("--enforceChirality ", Options["--enforceChirality"], "yes no")
1000
1001 MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
1002 MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "sdf sd mol smi txt csv tsv")
1003
1004 MiscUtil.ValidateOptionFilePath("-r, --reffile", Options["--reffile"])
1005 MiscUtil.ValidateOptionFileExt("-r, --reffile", Options["--reffile"], "sdf sd mol")
1006
1007 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "sdf sd")
1008 MiscUtil.ValidateOptionsOutputFileOverwrite(
1009 "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
1010 )
1011 MiscUtil.ValidateOptionsDistinctFileNames(
1012 "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
1013 )
1014
1015 MiscUtil.ValidateOptionIntegerValue("--maxConfs", Options["--maxConfs"], {">": 0})
1016
1017 MiscUtil.ValidateOptionTextValue("--mp", Options["--mp"], "yes no")
1018 MiscUtil.ValidateOptionTextValue("-q, --quiet", Options["--quiet"], "yes no")
1019
1020 MiscUtil.ValidateOptionTextValue("-r, --removeHydrogens", Options["--removeHydrogens"], "yes no")
1021
1022 MiscUtil.ValidateOptionTextValue("-u, --useTethers", Options["--useTethers"], "yes no")
1023
1024
1025 # Setup a usage string for docopt...
1026 _docoptUsage_ = """
1027 RDKitGenerateConstrainedConformers.py - Generate constrained molecular conformations
1028
1029 Usage:
1030 RDKitGenerateConstrainedConformers.py [--addHydrogens <yes or no>] [--conformerGenerator <SDG, KDG, ETDG, ETKDG, ETKDGv2>]
1031 [--forceField <UFF, or MMFF>] [--forceFieldMMFFVariant <MMFF94 or MMFF94s>]
1032 [--energyOut <yes or no>] [--enforceChirality <yes or no>] [--energyRMSDCutoff <number>]
1033 [--energyWindow <number> ] [--infileParams <Name,Value,...>] [--maxConfs <number>]
1034 [--mcsParams <Name,Value,...>] [--mp <yes or no>] [--mpParams <Name,Value,...>]
1035 [ --outfileParams <Name,Value,...> ] [--overwrite] [--quiet <yes or no>] [ --removeHydrogens <yes or no>]
1036 [--scaffold <auto or SMARTS>] [--scaffoldRMSDOut <yes or no>] [--useTethers <yes or no>]
1037 [-w <dir>] -i <infile> -r <reffile> -o <outfile>
1038 RDKitGenerateConstrainedConformers.py -h | --help | -e | --examples
1039
1040 Description:
1041 Generate molecular conformations by performing a constrained energy minimization
1042 against a reference molecule. An initial set of 3D conformers are generated for the input
1043 molecules using distance geometry. A common core scaffold, corresponding to
1044 a Maximum Common Substructure (MCS) or an explicit SMARTS pattern, is identified
1045 between a pair of input and reference molecules. The core scaffold atoms in input
1046 molecules are aligned against the same atoms in the reference molecule. The energy
1047 of aligned structures are minimized using the forcefield to generate the final 3D structures.
1048
1049 The supported input file formats are: Mol (.mol), SD (.sdf, .sd), SMILES (.smi,
1050 .csv, .tsv, .txt)
1051
1052 The supported output file formats are: SD (.sdf, .sd)
1053
1054 Options:
1055 -a, --addHydrogens <yes or no> [default: yes]
1056 Add hydrogens before minimization.
1057 -c, --conformerGenerator <text> [default: ETKDGv2]
1058 Conformation generation methodology for generating initial 3D coordinates
1059 for molecules in input file. A common core scaffold is identified between a
1060 a pair of input and reference molecules. The atoms in common core scaffold
1061 of input molecules are aligned against the reference molecule followed by
1062 energy minimization to generate final 3D structure.
1063
1064 The possible values along with a brief description are shown below:
1065
1066 SDG: Standard Distance Geometry
1067 KDG: basic Knowledge-terms with Distance Geometry
1068 ETDG: Experimental Torsion-angle preference with Distance Geometry
1069 ETKDG: Experimental Torsion-angle preference along with basic
1070 Knowledge-terms and Distance Geometry [Ref 129]
1071 ETKDGv2: Experimental Torsion-angle preference along with basic
1072 Knowledge-terms and Distance Geometry [Ref 167]
1073 -f, --forceField <UFF, MMFF> [default: MMFF]
1074 Forcefield method to use for constrained energy minimization. Possible values:
1075 Universal Force Field (UFF) [ Ref 81 ] or Merck Molecular Mechanics Force
1076 Field [ Ref 83-87 ] .
1077 --forceFieldMMFFVariant <MMFF94 or MMFF94s> [default: MMFF94]
1078 Variant of MMFF forcefield to use for energy minimization.
1079 --energyOut <yes or no> [default: No]
1080 Write out energy values.
1081 --enforceChirality <yes or no> [default: Yes]
1082 Enforce chirality for defined chiral centers.
1083 --energyRMSDCutoff <number> [default: 0.5]
1084 RMSD cutoff for retaining conformations after embedding and energy minimization.
1085 Possible values: A number or None
1086
1087 The default is to keep only those conformations which are different from the
1088 lowest energy conformation by the specified RMSD cutoff. The None value may
1089 be used to keep all minimized conformations with in the specified energy window
1090 from the lowest energy conformation. The lowest energy conformation is always
1091 retained.
1092 --energyWindow <number> [default: 20]
1093 Energy window in kcal/mol for selecting conformers.
1094 -e, --examples
1095 Print examples.
1096 -h, --help
1097 Print this help message.
1098 -i, --infile <infile>
1099 Input file name.
1100 --infileParams <Name,Value,...> [default: auto]
1101 A comma delimited list of parameter name and value pairs for reading
1102 molecules from files. The supported parameter names for different file
1103 formats, along with their default values, are shown below:
1104
1105 SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes
1106
1107 SMILES: smilesColumn,1,smilesNameColumn,2,smilesDelimiter,space,
1108 smilesTitleLine,auto,sanitize,yes
1109
1110 Possible values for smilesDelimiter: space, comma or tab.
1111 --maxConfs <number> [default: 50]
1112 Maximum number of conformations to generate for each molecule by conformation
1113 generation methodology for initial 3D coordinates. A constrained minimization is
1114 performed using the specified forcefield and the lowest energy conformation is written
1115 to the output file.
1116 --mcsParams <Name,Value,...> [default: auto]
1117 Parameter values to use for identifying a maximum common substructure
1118 (MCS) in between a pair of reference and input molecules.In general, it is a
1119 comma delimited list of parameter name and value pairs. The supported
1120 parameter names along with their default values are shown below:
1121
1122 atomCompare,CompareElements,bondCompare,CompareOrder,
1123 maximizeBonds,yes,matchValences,yes,matchChiralTag,no,
1124 minNumAtoms,1,minNumBonds,0,ringMatchesRingOnly,yes,
1125 completeRingsOnly,yes,threshold,1.0,timeOut,3600,seedSMARTS,none
1126
1127 Possible values for atomCompare: CompareAny, CompareElements,
1128 CompareIsotopes. Possible values for bondCompare: CompareAny,
1129 CompareOrder, CompareOrderExact.
1130
1131 A brief description of MCS parameters taken from RDKit documentation is
1132 as follows:
1133
1134 atomCompare - Controls match between two atoms
1135 bondCompare - Controls match between two bonds
1136 maximizeBonds - Maximize number of bonds instead of atoms
1137 matchValences - Include atom valences in the MCS match
1138 matchChiralTag - Include atom chirality in the MCS match
1139 minNumAtoms - Minimum number of atoms in the MCS match
1140 minNumBonds - Minimum number of bonds in the MCS match
1141 ringMatchesRingOnly - Ring bonds only match other ring bonds
1142 completeRingsOnly - Partial rings not allowed during the match
1143 threshold - Fraction of the dataset that must contain the MCS
1144 seedSMARTS - SMARTS string as the seed of the MCS
1145 timeout - Timeout for the MCS calculation in seconds
1146
1147 --mp <yes or no> [default: no]
1148 Use multiprocessing.
1149
1150 By default, input data is retrieved in a lazy manner via mp.Pool.imap()
1151 function employing lazy RDKit data iterable. This allows processing of
1152 arbitrary large data sets without any additional requirements memory.
1153
1154 All input data may be optionally loaded into memory by mp.Pool.map()
1155 before starting worker processes in a process pool by setting the value
1156 of 'inputDataMode' to 'InMemory' in '--mpParams' option.
1157
1158 A word to the wise: The default 'chunkSize' value of 1 during 'Lazy' input
1159 data mode may adversely impact the performance. The '--mpParams' section
1160 provides additional information to tune the value of 'chunkSize'.
1161 --mpParams <Name,Value,...> [default: auto]
1162 A comma delimited list of parameter name and value pairs to configure
1163 multiprocessing.
1164
1165 The supported parameter names along with their default and possible
1166 values are shown below:
1167
1168 chunkSize, auto
1169 inputDataMode, Lazy [ Possible values: InMemory or Lazy ]
1170 numProcesses, auto [ Default: mp.cpu_count() ]
1171
1172 These parameters are used by the following functions to configure and
1173 control the behavior of multiprocessing: mp.Pool(), mp.Pool.map(), and
1174 mp.Pool.imap().
1175
1176 The chunkSize determines chunks of input data passed to each worker
1177 process in a process pool by mp.Pool.map() and mp.Pool.imap() functions.
1178 The default value of chunkSize is dependent on the value of 'inputDataMode'.
1179
1180 The mp.Pool.map() function, invoked during 'InMemory' input data mode,
1181 automatically converts RDKit data iterable into a list, loads all data into
1182 memory, and calculates the default chunkSize using the following method
1183 as shown in its code:
1184
1185 chunkSize, extra = divmod(len(dataIterable), len(numProcesses) * 4)
1186 if extra: chunkSize += 1
1187
1188 For example, the default chunkSize will be 7 for a pool of 4 worker processes
1189 and 100 data items.
1190
1191 The mp.Pool.imap() function, invoked during 'Lazy' input data mode, employs
1192 'lazy' RDKit data iterable to retrieve data as needed, without loading all the
1193 data into memory. Consequently, the size of input data is not known a priori.
1194 It's not possible to estimate an optimal value for the chunkSize. The default
1195 chunkSize is set to 1.
1196
1197 The default value for the chunkSize during 'Lazy' data mode may adversely
1198 impact the performance due to the overhead associated with exchanging
1199 small chunks of data. It is generally a good idea to explicitly set chunkSize to
1200 a larger value during 'Lazy' input data mode, based on the size of your input
1201 data and number of processes in the process pool.
1202
1203 The mp.Pool.map() function waits for all worker processes to process all
1204 the data and return the results. The mp.Pool.imap() function, however,
1205 returns the the results obtained from worker processes as soon as the
1206 results become available for specified chunks of data.
1207
1208 The order of data in the results returned by both mp.Pool.map() and
1209 mp.Pool.imap() functions always corresponds to the input data.
1210 -o, --outfile <outfile>
1211 Output file name.
1212 --outfileParams <Name,Value,...> [default: auto]
1213 A comma delimited list of parameter name and value pairs for writing
1214 molecules to files. The supported parameter names for different file
1215 formats, along with their default values, are shown below:
1216
1217 SD: kekulize,yes,forceV3000,no
1218
1219 --overwrite
1220 Overwrite existing files.
1221 -q, --quiet <yes or no> [default: no]
1222 Use quiet mode. The warning and information messages will not be printed.
1223 -r, --reffile <reffile>
1224 Reference input file name containing a 3D reference molecule. A common
1225 core scaffold must be present in a pair of an input and reference molecules.
1226 Otherwise, no constrained minimization is performed on the input molecule.
1227 --removeHydrogens <yes or no> [default: Yes]
1228 Remove hydrogens after minimization.
1229 -s, --scaffold <auto or SMARTS> [default: auto]
1230 Common core scaffold between a pair of input and reference molecules used for
1231 constrained minimization of molecules in input file. Possible values: Auto or a
1232 valid SMARTS pattern. The common core scaffold is automatically detected
1233 corresponding to the Maximum Common Substructure (MCS) between a pair of
1234 reference and input molecules. A valid SMARTS pattern may be optionally specified
1235 for the common core scaffold.
1236 --scaffoldRMSDOut <yes or no> [default: No]
1237 Write out RMSD value for common core alignment between a pair of input and
1238 reference molecules.
1239 -u, --useTethers <yes or no> [default: yes]
1240 Use tethers to optimize the final conformation by applying a series of extra forces
1241 to align matching atoms to the positions of the core atoms. Otherwise, use simple
1242 distance constraints during the optimization.
1243 -w, --workingdir <dir>
1244 Location of working directory which defaults to the current directory.
1245
1246 Examples:
1247 To generate conformers by performing constrained energy minimization for molecules
1248 in a SMILES file against a reference 3D molecule in a SD file using a common core
1249 scaffold between pairs of input and reference molecules identified using MCS,
1250 generating up to 50 conformations using ETKDG methodology followed by MMFF
1251 forcefield minimization within energy window of 20 kcal/mol and RMSD of greater
1252 than 0.5 from the lowest energy conformation, and write out a SD file:
1253
1254 % RDKitGenerateConstrainedConformers.py -i SampleSeriesD3R.smi
1255 -r SampleSeriesRef3D.sdf -o SampleOut.sdf
1256
1257 To rerun the first example in a quiet mode and write out a SD file, type:
1258
1259 % RDKitGenerateConstrainedConformers.py -q yes -i SampleSeriesD3R.smi
1260 -r SampleSeriesRef3D.sdf -o SampleOut.sdf
1261
1262 To rerun the first example in multiprocessing mode on all available CPUs
1263 without loading all data into memory and write out a SD file, type:
1264
1265 % RDKitGenerateConstrainedConformers.py --mp yes -i SampleSeriesD3R.smi
1266 -r SampleSeriesRef3D.sdf -o SampleOut.sdf
1267
1268 To run the first example in multiprocessing mode on all available CPUs
1269 by loading all data into memory and write out a SD file, type:
1270
1271 % RDKitGenerateConstrainedConformers.py --mp yes --mpParams
1272 "inputDataMode,InMemory" -i SampleSeriesD3R.smi
1273 -r SampleSeriesRef3D.sdf -o SampleOut.sdf
1274
1275 To rerun the first example in multiprocessing mode on specific number of
1276 CPUs and chunk size without loading all data into memory and write out a SD file,
1277 type:
1278
1279 % RDKitGenerateConstrainedConformers.py --mp yes --mpParams
1280 "inputDataMode,Lazy,numProcesses,4,chunkSize,8"
1281 -i SampleSeriesD3R.smi -r SampleSeriesRef3D.sdf -o SampleOut.sdf
1282
1283 To rerun the first example using an explicit SMARTS string for a common core
1284 scaffold and write out a SD file, type:
1285
1286 % RDKitGenerateConstrainedConformers.py --scaffold
1287 "c2cc(-c3nc(N)ncc3)cn2" -i SampleSeriesD3R.smi
1288 -r SampleSeriesRef3D.sdf -o SampleOut.sdf
1289
1290 To rerun the first example using molecules in a CSV SMILES file, SMILES
1291 strings in column 1, name in column2, and write out a SD file, type:
1292
1293 % RDKitGenerateConstrainedConformers.py --infileParams
1294 "smilesDelimiter,comma,smilesTitleLine,yes,smilesColumn,1,
1295 smilesNameColumn,2" -i SampleSeriesD3R.csv -r SampleSeriesRef3D.sdf
1296 -o SampleOut.sdf
1297
1298 To generate constrained conformers for molecules in a SD file against a reference
1299 3D molecule in a SD file using a common core scaffold between pairs of input and
1300 reference molecules identified using MCS, generating up to 10 conformations
1301 using SDG methodology followed by UFF forcefield minimization, conformations
1302 with in an energy window of 10 kcal/mol and RMSD of greater that 1, and write out
1303 a SD file containing minimum energy structure along with energy and embed RMS
1304 values corresponding to each constrained molecule, type:
1305
1306 % RDKitGenerateConstrainedConformers.py --maxConfs 10 -c SDG -f UFF
1307 --scaffoldRMSDOut yes --energyOut yes --energyRMSDCutoff 1.0
1308 --energyWindow 10 -i SampleSeriesD3R.sdf -r SampleSeriesRef3D.sdf
1309 -o SampleOut.sdf
1310
1311 Author:
1312 Manish Sud(msud@san.rr.com)
1313
1314 See also:
1315 RDKitCalculateRMSD.py, RDKitCalculateMolecularDescriptors.py, RDKitCompareMoleculeShapes.py,
1316 RDKitConvertFileFormat.py, RDKitGenerateConformers.py, RDKitPerformConstrainedMinimization.py
1317
1318 Copyright:
1319 Copyright (C) 2026 Manish Sud. All rights reserved.
1320
1321 The functionality available in this script is implemented using RDKit, an
1322 open source toolkit for cheminformatics developed by Greg Landrum.
1323
1324 This file is part of MayaChemTools.
1325
1326 MayaChemTools is free software; you can redistribute it and/or modify it under
1327 the terms of the GNU Lesser General Public License as published by the Free
1328 Software Foundation; either version 3 of the License, or (at your option) any
1329 later version.
1330
1331 """
1332
1333 if __name__ == "__main__":
1334 main()