1 //
2 //  Copyright (C) 2017-2020 Greg Landrum
3 //
4 //   @@ All Rights Reserved @@
5 //  This file is part of the RDKit.
6 //  The contents are covered by the terms of the BSD license
7 //  which is included in the file license.txt, found at the root
8 //  of the RDKit source tree.
9 //
10 
11 #include <RDGeneral/RDLog.h>
12 #include <GraphMol/RDKitBase.h>
13 #include <GraphMol/Substruct/SubstructMatch.h>
14 #include <GraphMol/MolTransforms/MolTransforms.h>
15 #include <Geometry/Transform3D.h>
16 #include <cstdlib>
17 
18 #include "coordgen/sketcherMinimizer.h"
19 #include "coordgen/CoordgenFragmenter.h"
20 
21 namespace RDKit {
22 namespace CoordGen {
23 
24 struct CoordGenParams {
25   const float sketcherCoarsePrecision = 0.01;
26   const float sketcherStandardPrecision = SKETCHER_STANDARD_PRECISION;
27   const float sketcherBestPrecision = SKETCHER_BEST_PRECISION;
28   const float sketcherQuickPrecision = SKETCHER_QUICK_PRECISION;
29   RDGeom::INT_POINT2D_MAP
30       coordMap;  // coordinates for fixing particular atoms of a template
31   const ROMol* templateMol = nullptr;  // a molecule to use as a template
32   double coordgenScaling = 50.0;       // at the time this was written, coordgen
33   // returned coordinates with a single bond
34   // length of 50.
35   std::string templateFileDir = "";
36   float minimizerPrecision =
37       sketcherCoarsePrecision;     // controls sketch precision
38   bool dbg_useConstrained = true;  // debugging
39   bool dbg_useFixed = false;       // debugging
40   bool minimizeOnly = false;       // don't actually generate full coords
41   bool treatNonterminalBondsToMetalAsZeroOrder =
42       false;  // set non-terminal bonds to metal atoms to be zero-order bonds
43 };
44 
45 static CoordGenParams defaultParams;
46 
47 //! Generates a 2D conformer for a molecule and replaces the existing conformers
48 /*!
49   This call uses the CoordGen library from Schroedinger
50 
51   \param mol     the molecule we are working with
52   \param params  a pointer to a parameter object
53 
54 */
55 template <typename T>
56 unsigned int addCoords(T& mol, const CoordGenParams* params = nullptr) {
57   if (!params) params = &defaultParams;
58   // FIX: the default value of this should be handled once in a threadsafe way
59   std::string templateFileDir;
60   if (params->templateFileDir != "") {
61     templateFileDir = params->templateFileDir;
62   } else {
63     auto rdbase = std::getenv("RDBASE");
64     if (rdbase != nullptr) {
65       templateFileDir += rdbase;
66       templateFileDir += "/Data/";
67     }
68   }
69 
70   if (params->minimizeOnly && !mol.getNumConformers()) {
71     throw ValueErrorException(
72         "minimizeOnly set but molecule has no conformers");
73   }
74 
75   double scaleFactor = params->coordgenScaling;
76 
77   sketcherMinimizer minimizer(params->minimizerPrecision);
78   auto min_mol = new sketcherMinimizerMolecule();
79 
80   minimizer.setTreatNonterminalBondsToMetalAsZOBs(
81       params->treatNonterminalBondsToMetalAsZeroOrder);
82 
83   // FIX: only do this check once.
84   // std::cerr << "  TEMPLATES: " << templateFileDir << std::endl;
85   if (templateFileDir != "") {
86     minimizer.setTemplateFileDir(templateFileDir);
87   }
88   bool hasTemplateMatch = false;
89   MatchVectType mv;
90   if (!params->minimizeOnly && params->templateMol &&
91       params->templateMol->getNumConformers() == 1) {
92     if (SubstructMatch(mol, *(params->templateMol), mv)) {
93       hasTemplateMatch = true;
94     }
95   }
96 
97   // if we're doing coordinate minimization it makes our life easier to
98   // start by translating to the origin
99   RDGeom::Point3D centroid{0.0, 0.0, 0.0};
100   if (params->minimizeOnly) {
101     auto conf = mol.getConformer();
102     centroid = MolTransforms::computeCentroid(conf);
103   }
104 
105   std::vector<sketcherMinimizerAtom*> ats(mol.getNumAtoms());
106   for (auto atit = mol.beginAtoms(); atit != mol.endAtoms(); ++atit) {
107     auto oatom = *atit;
108     auto atom = min_mol->addNewAtom();
109     atom->molecule = min_mol;  // seems like this should be in addNewAtom()
110     atom->atomicNumber = oatom->getAtomicNum();
111     atom->charge = oatom->getFormalCharge();
112     if (params->minimizeOnly) {
113       auto coords = mol.getConformer().getAtomPos(oatom->getIdx());
114       coords -= centroid;
115       atom->coordinates = sketcherMinimizerPointF(coords.x * scaleFactor,
116                                                   coords.y * scaleFactor);
117       atom->fixed = false;
118       atom->constrained = false;
119     } else if (hasTemplateMatch || params->coordMap.find(oatom->getIdx()) !=
120                                        params->coordMap.end()) {
121       atom->constrained = params->dbg_useConstrained;
122       atom->fixed = params->dbg_useFixed;
123       if (hasTemplateMatch) {
124         for (auto& pr : mv) {
125           if (pr.second == static_cast<int>(oatom->getIdx())) {
126             const RDGeom::Point3D& coords =
127                 params->templateMol->getConformer().getAtomPos(pr.first);
128             atom->templateCoordinates = sketcherMinimizerPointF(
129                 coords.x * scaleFactor, coords.y * scaleFactor);
130             break;
131           }
132         }
133       } else {
134         const RDGeom::Point2D& coords =
135             params->coordMap.find(oatom->getIdx())->second;
136         atom->templateCoordinates = sketcherMinimizerPointF(
137             coords.x * scaleFactor, coords.y * scaleFactor);
138       }
139     }
140     ats[oatom->getIdx()] = atom;
141   }
142 
143   // coordgen has its own ideas about what bond lengths should be
144   // if we are doing minimizeOnly we want to do a rigid scaling after the
145   // minimization in order to try and get as close to where we started as
146   // possible
147   double singleBondLenAccum = 0.0;
148   double bondLenAccum = 0.0;
149   unsigned nSingle = 0;
150 
151   std::vector<sketcherMinimizerBond*> bnds(mol.getNumBonds());
152   for (auto bndit = mol.beginBonds(); bndit != mol.endBonds(); ++bndit) {
153     auto obnd = *bndit;
154     auto bnd = min_mol->addNewBond(ats[obnd->getBeginAtomIdx()],
155                                    ats[obnd->getEndAtomIdx()]);
156     // FIX: This is no doubt wrong
157     switch (obnd->getBondType()) {
158       case Bond::SINGLE:
159         bnd->bondOrder = 1;
160         if (params->minimizeOnly) {
161           ++nSingle;
162           singleBondLenAccum +=
163               (mol.getConformer().getAtomPos(obnd->getBeginAtomIdx()) -
164                mol.getConformer().getAtomPos(obnd->getEndAtomIdx()))
165                   .length();
166         }
167         break;
168       case Bond::DOUBLE:
169         bnd->bondOrder = 2;
170         break;
171       case Bond::TRIPLE:
172         bnd->bondOrder = 3;
173         break;
174       case Bond::AROMATIC:
175         bnd->bondOrder = 1;
176         break;
177       default:
178         BOOST_LOG(rdWarningLog) << "unrecognized bond type";
179     }
180     bnds[obnd->getIdx()] = bnd;
181     if (params->minimizeOnly) {
182       bondLenAccum += (mol.getConformer().getAtomPos(obnd->getBeginAtomIdx()) -
183                        mol.getConformer().getAtomPos(obnd->getEndAtomIdx()))
184                           .length();
185     }
186   }
187   double avgBondLen = 1.0;
188   if (params->minimizeOnly) {
189     if (nSingle) {
190       avgBondLen = singleBondLenAccum / nSingle;
191     } else if (mol.getNumBonds()) {
192       avgBondLen = bondLenAccum / mol.getNumBonds();
193     }
194   }
195 
196   // setup double bond stereo
197   min_mol->assignBondsAndNeighbors(ats, bnds);
198   for (auto bndit = mol.beginBonds(); bndit != mol.endBonds(); ++bndit) {
199     auto obnd = *bndit;
200     if (obnd->getBondType() != Bond::DOUBLE ||
201         obnd->getStereo() <= Bond::STEREOANY ||
202         obnd->getStereo() > Bond::STEREOTRANS)
203       continue;
204 
205     sketcherMinimizerBondStereoInfo sinfo;
206     sinfo.atom1 = ats[obnd->getStereoAtoms()[0]];
207     sinfo.atom2 = ats[obnd->getStereoAtoms()[1]];
208     sinfo.stereo = (obnd->getStereo() == Bond::STEREOZ ||
209                     obnd->getStereo() == Bond::STEREOCIS)
210                        ? sketcherMinimizerBondStereoInfo::cis
211                        : sketcherMinimizerBondStereoInfo::trans;
212     auto bnd = bnds[obnd->getIdx()];
213     bnd->setStereoChemistry(sinfo);
214     bnd->setAbsoluteStereoFromStereoInfo();
215   }
216 
217   minimizer.initialize(min_mol);
218   if (!params->minimizeOnly) {
219     minimizer.runGenerateCoordinates();
220   } else {
221     CoordgenFragmenter::splitIntoFragments(min_mol);
222     minimizer.m_minimizer.minimizeMolecule(min_mol);
223 
224     // Hook the fragments into the minimizer so that they
225     // get cleaned up when the minimizer is terminated
226     minimizer._fragments = min_mol->getFragments();
227   }
228   auto conf = new Conformer(mol.getNumAtoms());
229   for (size_t i = 0; i < mol.getNumAtoms(); ++i) {
230     auto coords = RDGeom::Point3D(ats[i]->coordinates.x() / scaleFactor,
231                                   ats[i]->coordinates.y() / scaleFactor, 0.0);
232     if (params->minimizeOnly) {
233       // readjust the average bond length from 1 (coordgen's target)
234       // to whatever we started with
235       coords *= avgBondLen;
236       coords += centroid;
237     }
238     conf->setAtomPos(i, coords);
239     // std::cerr << atom->coordinates << std::endl;
240   }
241   conf->set3D(false);
242   mol.clearConformers();
243   auto res = mol.addConformer(conf, true);
244   if (!params->minimizeOnly && params->coordMap.empty() &&
245       !params->templateMol) {
246     // center the coordinates
247     RDGeom::Transform3D tf;
248     auto centroid = MolTransforms::computeCentroid(*conf);
249     centroid *= -1;
250     tf.SetTranslation(centroid);
251     MolTransforms::transformConformer(*conf, tf);
252   }
253   return res;
254 }
255 }  // end of namespace CoordGen
256 }  // namespace RDKit
257