1 /*
2  * PCMSolver, an API for the Polarizable Continuum Model
3  * Copyright (C) 2020 Roberto Di Remigio, Luca Frediani and contributors.
4  *
5  * This file is part of PCMSolver.
6  *
7  * PCMSolver is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Lesser General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * PCMSolver is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public License
18  * along with PCMSolver.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  * For information on the complete list of contributors to the
21  * PCMSolver API, see: <http://pcmsolver.readthedocs.io/>
22  */
23 
24 #include <sys/types.h>
25 #include "GePolCavity.hpp"
26 
27 #include <algorithm>
28 #include <cctype>
29 #include <fstream>
30 #include <iomanip>
31 #include <iostream>
32 #include <sstream>
33 #include <string>
34 #include <vector>
35 
36 #include <unistd.h>
37 
38 #include "Config.hpp"
39 
40 #include <Eigen/Core>
41 
42 #include "CavityData.hpp"
43 #include "utils/MathUtils.hpp"
44 #include "utils/Sphere.hpp"
45 #include "utils/Symmetry.hpp"
46 
47 namespace pcm {
48 namespace cavity {
GePolCavity(const Molecule & molec,double a,double pr,double minR,const std::string & suffix)49 GePolCavity::GePolCavity(const Molecule & molec,
50                          double a,
51                          double pr,
52                          double minR,
53                          const std::string & suffix)
54     : ICavity(molec), averageArea(a), probeRadius(pr), minimalRadius(minR) {
55   TIMER_ON("GePolCavity::build from Molecule object");
56   build(suffix, 50000, 1000, 100000);
57   TIMER_OFF("GePolCavity::build from Molecule object");
58 }
59 
GePolCavity(const Sphere & sph,double a,double pr,double minR,const std::string & suffix)60 GePolCavity::GePolCavity(const Sphere & sph,
61                          double a,
62                          double pr,
63                          double minR,
64                          const std::string & suffix)
65     : ICavity(sph), averageArea(a), probeRadius(pr), minimalRadius(minR) {
66   TIMER_ON("GePolCavity::build from single sphere");
67   build(suffix, 50000, 1000, 100000);
68   TIMER_OFF("GePolCavity::build from single sphere");
69 }
70 
GePolCavity(const std::vector<Sphere> & sph,double a,double pr,double minR,const std::string & suffix)71 GePolCavity::GePolCavity(const std::vector<Sphere> & sph,
72                          double a,
73                          double pr,
74                          double minR,
75                          const std::string & suffix)
76     : ICavity(sph), averageArea(a), probeRadius(pr), minimalRadius(minR) {
77   TIMER_ON("GePolCavity::build from list of spheres");
78   build(suffix, 50000, 1000, 100000);
79   TIMER_OFF("GePolCavity::build from list of spheres");
80 }
81 
build(const std::string & suffix,int maxts,int maxsph,int maxvert)82 void GePolCavity::build(const std::string & suffix,
83                         int maxts,
84                         int maxsph,
85                         int maxvert) {
86 
87   // This is a wrapper for the generatecavity_cpp function defined in the Fortran
88   // code PEDRA.
89   // Here we allocate the necessary arrays to be passed to PEDRA, in particular we
90   // allow
91   // for the insertion of additional spheres as in the most general formulation of
92   // the
93   // GePol algorithm.
94 
95   double * xtscor = new double[maxts];
96   double * ytscor = new double[maxts];
97   double * ztscor = new double[maxts];
98   double * ar = new double[maxts];
99   double * xsphcor = new double[maxts];
100   double * ysphcor = new double[maxts];
101   double * zsphcor = new double[maxts];
102   double * rsph = new double[maxts];
103   int * nvert = new int[maxts];
104   double * vert = new double[30 * maxts];
105   double * centr = new double[30 * maxts];
106   int * isphe = new int[maxts];
107 
108   // Clean-up possible heap-crap
109   std::fill_n(xtscor, maxts, 0.0);
110   std::fill_n(ytscor, maxts, 0.0);
111   std::fill_n(ztscor, maxts, 0.0);
112   std::fill_n(ar, maxts, 0.0);
113   std::fill_n(xsphcor, maxts, 0.0);
114   std::fill_n(ysphcor, maxts, 0.0);
115   std::fill_n(zsphcor, maxts, 0.0);
116   std::fill_n(rsph, maxts, 0.0);
117   std::fill_n(nvert, maxts, 0);
118   std::fill_n(vert, 30 * maxts, 0.0);
119   std::fill_n(centr, 30 * maxts, 0.0);
120   std::fill_n(isphe, maxts, 0);
121 
122   int nts = 0;
123   int ntsirr = 0;
124 
125   // If there's an overflow in the number of spheres PEDRA will die.
126   // The maximum number of spheres in PEDRA is set to 200 (primitive+additional)
127   // so the integer here declared is just to have enough space C++ side to pass
128   // everything back.
129   int maxAddedSpheres = 200;
130 
131   // Allocate vectors of size equal to nSpheres + maxAddedSpheres where
132   // maxAddedSpheres is the
133   // maximum number of spheres we allow the algorithm to add to our original set.
134   // If this number is exceeded, then the algorithm crashes (should look into
135   // this...)
136   // After the cavity is generated we will update ALL the class data members, both
137   // related
138   // to spheres and finite elements so that the cavity is fully formed.
139 
140   Eigen::VectorXd xv = Eigen::VectorXd::Zero(nSpheres_ + maxAddedSpheres);
141   Eigen::VectorXd yv = Eigen::VectorXd::Zero(nSpheres_ + maxAddedSpheres);
142   Eigen::VectorXd zv = Eigen::VectorXd::Zero(nSpheres_ + maxAddedSpheres);
143   Eigen::VectorXd radii_scratch =
144       Eigen::VectorXd::Zero(nSpheres_ + maxAddedSpheres); // Not to be confused with
145                                                           // the data member
146   // inherited from ICavity!!!
147 
148   for (int i = 0; i < nSpheres_; ++i) {
149     for (int j = 0; j < 3; ++j) {
150       xv(i) = sphereCenter_(0, i);
151       yv(i) = sphereCenter_(1, i);
152       zv(i) = sphereCenter_(2, i);
153     }
154     radii_scratch(i) = sphereRadius_(i);
155   }
156 
157   double * xe = xv.data();
158   double * ye = yv.data();
159   double * ze = zv.data();
160 
161   double * rin = radii_scratch.data();
162   double * mass = new double[molecule_.nAtoms()];
163   for (size_t i = 0; i < molecule_.nAtoms(); ++i) {
164     mass[i] = molecule_.masses(i);
165   }
166 
167   addedSpheres = 0;
168   // Number of generators and generators of the point group
169   int nr_gen = molecule_.pointGroup().nrGenerators();
170   int gen1 = molecule_.pointGroup().generators(0);
171   int gen2 = molecule_.pointGroup().generators(1);
172   int gen3 = molecule_.pointGroup().generators(2);
173 
174   std::stringstream pedra;
175   pedra << "PEDRA.OUT_" << suffix << "_" << getpid();
176   int len_f_pedra = std::strlen(pedra.str().c_str());
177   // Go PEDRA, Go!
178   TIMER_ON("GePolCavity::generatecavity_cpp");
179   generatecavity_cpp(&maxts,
180                      &maxsph,
181                      &maxvert,
182                      xtscor,
183                      ytscor,
184                      ztscor,
185                      ar,
186                      xsphcor,
187                      ysphcor,
188                      zsphcor,
189                      rsph,
190                      &nts,
191                      &ntsirr,
192                      &nSpheres_,
193                      &addedSpheres,
194                      xe,
195                      ye,
196                      ze,
197                      rin,
198                      mass,
199                      &averageArea,
200                      &probeRadius,
201                      &minimalRadius,
202                      &nr_gen,
203                      &gen1,
204                      &gen2,
205                      &gen3,
206                      nvert,
207                      vert,
208                      centr,
209                      isphe,
210                      pedra.str().c_str(),
211                      &len_f_pedra);
212   TIMER_OFF("GePolCavity::generatecavity_cpp");
213 
214   // The "intensive" part of updating the spheres related class data members will be
215   // of course
216   // executed iff addedSpheres != 0
217   if (addedSpheres != 0) {
218     // Save the number of original spheres
219     int orig = nSpheres_;
220     // Update the nSpheres
221     nSpheres_ += addedSpheres;
222     // Resize sphereRadius and sphereCenter...
223     sphereRadius_.resize(nSpheres_);
224     sphereCenter_.resize(Eigen::NoChange, nSpheres_);
225     // Transfer radii and centers.
226     // Eigen has no push_back function, so we need to traverse all the spheres...
227     sphereRadius_ = radii_scratch.head(nSpheres_);
228     for (int i = 0; i < nSpheres_; ++i) {
229       sphereCenter_(0, i) = xv(i);
230       sphereCenter_(1, i) = yv(i);
231       sphereCenter_(2, i) = zv(i);
232     }
233     // Now grow the vector<Sphere> containing the list of spheres
234     for (int i = orig; i < nSpheres_; ++i) {
235       spheres_.push_back(Sphere(sphereCenter_.col(i), sphereRadius_(i)));
236     }
237   }
238 
239   // Now take care of updating the rest of the cavity info.
240   // We prune the list of tesserae coming out of PEDRA to remove the ones that
241   // have area smaller than 1.0e-4 AU^2 these finite elements can breake
242   // symmetric positive-definiteness of the S matrix.
243   Eigen::MatrixXd centers = Eigen::MatrixXd::Zero(3, nts);
244   Eigen::MatrixXd sphereCenters = Eigen::MatrixXd::Zero(3, nts);
245   Eigen::VectorXd areas = Eigen::VectorXd::Zero(nts);
246   Eigen::VectorXd radii = Eigen::VectorXd::Zero(nts);
247   // Filtering array
248   Eigen::Matrix<bool, 1, Eigen::Dynamic> filter =
249       Eigen::Matrix<bool, 1, Eigen::Dynamic>::Zero(nts);
250   PCMSolverIndex retval = 0;
251   for (PCMSolverIndex i = 0; i < nts; ++i) {
252     if (std::abs(ar[i]) >= 1.0e-04) {
253       retval += 1;
254       centers.col(i) =
255           (Eigen::Vector3d() << xtscor[i], ytscor[i], ztscor[i]).finished();
256       sphereCenters.col(i) =
257           (Eigen::Vector3d() << xsphcor[i], ysphcor[i], zsphcor[i]).finished();
258       areas(i) = ar[i];
259       filter(i) = true;
260       radii(i) = rsph[i];
261     }
262   }
263   PCMSOLVER_ASSERT(filter.count() == retval);
264   // Resize data members and fill them up starting from the pruned arrays
265   // We first build a mask array, for the indices of the nonzero elements
266   elementCenter_ = utils::prune_zero_columns(centers, filter);
267   elementSphereCenter_ = utils::prune_zero_columns(sphereCenters, filter);
268   elementArea_ = utils::prune_vector(areas, filter);
269   elementRadius_ = utils::prune_vector(radii, filter);
270   nElements_ = retval;
271   pruned_ = nts - nElements_;
272   nIrrElements_ =
273       static_cast<PCMSolverIndex>(nElements_ / molecule_.pointGroup().nrIrrep());
274 
275   // Check that no points are overlapping exactly
276   // Do not perform float comparisons column by column.
277   // Instead form differences between columns and evaluate if they differ
278   // from zero by more than a fixed threshold.
279   // The indices of the equal elements are gathered in a std::pair and saved into a
280   // std::vector
281   double threshold = 1.0e-12;
282   std::vector<std::pair<PCMSolverIndex, PCMSolverIndex> > equal_elements;
283   for (PCMSolverIndex i = 0; i < nElements_; ++i) {
284     for (PCMSolverIndex j = i + 1; j < nElements_; ++j) {
285       Eigen::Vector3d difference = elementCenter_.col(i) - elementCenter_.col(j);
286       if (difference.isZero(threshold)) {
287         equal_elements.push_back(std::make_pair(i, j));
288       }
289     }
290   }
291   if (equal_elements.size() != 0) {
292     // Not sure that printing the list of pairs is actually of any help...
293     std::string list_of_pairs;
294     for (PCMSolverIndex i = 0; i < equal_elements.size(); ++i) {
295       list_of_pairs += "(" + std::to_string(equal_elements[i].first) + ", " +
296                        std::to_string(equal_elements[i].second) + ")\n";
297     }
298     // Prepare the error message:
299     std::string message = std::to_string(equal_elements.size()) +
300                           " cavity finite element centers overlap exactly!\n" +
301                           list_of_pairs;
302     PCMSOLVER_ERROR(message);
303   }
304   // Calculate normal vectors
305   elementNormal_.resize(Eigen::NoChange, nElements_);
306   elementNormal_ = elementCenter_ - elementSphereCenter_;
307   for (PCMSolverIndex i = 0; i < nElements_; ++i) {
308     elementNormal_.col(i) /= elementNormal_.col(i).norm();
309   }
310 
311   // Fill elements_ vector
312   for (PCMSolverIndex i = 0; i < nElements_; ++i) {
313     PCMSolverIndex i_off = i + 1;
314     bool irr = false;
315     // PEDRA puts the irreducible tesserae first
316     if (i < nIrrElements_)
317       irr = true;
318     Sphere sph(elementSphereCenter_.col(i), elementRadius_(i));
319     int nv = nvert[i];
320     int isph = isphe[i]; // Back to C++ indexing starting from 0
321     Eigen::Matrix3Xd vertices, arcs;
322     vertices.resize(Eigen::NoChange, nv);
323     arcs.resize(Eigen::NoChange, nv);
324     // Populate vertices and arcs
325     for (int j = 0; j < nv; ++j) {
326       PCMSolverIndex j_off = (j + 1) * nElements_ - 1;
327       for (PCMSolverIndex k = 0; k < 3; ++k) {
328         PCMSolverIndex k_off = (k + 1) * nElements_ * nv;
329         PCMSolverIndex offset = i_off + j_off + k_off;
330         vertices(k, j) = vert[offset];
331         arcs(k, j) = centr[offset];
332       }
333     }
334     elements_.push_back(Element(nv,
335                                 isph,
336                                 elementArea_(i),
337                                 elementCenter_.col(i),
338                                 elementNormal_.col(i),
339                                 irr,
340                                 sph,
341                                 vertices,
342                                 arcs));
343   }
344 
345   // Clean-up
346   delete[] xtscor;
347   delete[] ytscor;
348   delete[] ztscor;
349   delete[] ar;
350   delete[] xsphcor;
351   delete[] ysphcor;
352   delete[] zsphcor;
353   delete[] rsph;
354   delete[] nvert;
355   delete[] vert;
356   delete[] centr;
357   delete[] mass;
358   delete[] isphe;
359 
360   built = true;
361 
362   writeOFF(suffix);
363 }
364 
writeOFF(const std::string & suffix)365 void GePolCavity::writeOFF(const std::string & suffix) {
366   std::stringstream off;
367   off << "cavity.off_" << suffix << "_" << getpid();
368 
369   std::ofstream fout;
370   fout.open(off.str().c_str());
371 
372   int numv = 0;
373   for (PCMSolverIndex i = 0; i < nElements_; ++i) {
374     numv += elements_[i].nVertices();
375   }
376   fout << "COFF" << std::endl;
377   fout << numv << " " << nElements_ << " " << numv << std::endl;
378 
379   int k = 0;
380   double c1, c2, c3;
381   Eigen::MatrixXi ivts = Eigen::MatrixXi::Zero(nElements_, 10);
382   for (PCMSolverIndex i = 0; i < nElements_; ++i) {
383     if (i == 0)
384       fout << "# Sphere number " << elements_[i].iSphere() << std::endl;
385     c1 = 1.0;
386     c2 = 1.0;
387     c3 = 1.0;
388     for (int j = 0; j < elements_[i].nVertices(); ++j) {
389       ivts(i, j) = k;
390       k = k + 1;
391       fout << std::fixed << std::left << std::setfill('0') << std::setprecision(14)
392            << elements_[i].vertices()(0, j) << "    "
393            << elements_[i].vertices()(1, j) << "    "
394            << elements_[i].vertices()(2, j) << "    " << std::fixed << std::left
395            << std::setfill('0') << std::setprecision(4) << c1 << "    " << c2
396            << "    " << c3 << "    " << 0.75 << "  "
397            << " # Tess " << (i + 1) << std::endl;
398     }
399   }
400   for (PCMSolverIndex i = 0; i < nElements_; ++i) {
401     fout << elements_[i].nVertices() << " ";
402     for (int j = 0; j < elements_[i].nVertices(); ++j) {
403       fout << ivts(i, j) << " ";
404     }
405     fout << std::endl;
406   }
407 
408   fout.close();
409 }
410 
printCavity(std::ostream & os)411 std::ostream & GePolCavity::printCavity(std::ostream & os) {
412   os << "Cavity type: GePol" << std::endl;
413   os << "Average tesserae area = " << averageArea * bohr2ToAngstrom2() << " Ang^2"
414      << std::endl;
415   os << "Solvent probe radius = " << probeRadius * bohrToAngstrom() << " Ang"
416      << std::endl;
417   os << "Number of spheres = " << nSpheres_
418      << " [initial = " << nSpheres_ - addedSpheres << "; added = " << addedSpheres
419      << "]" << std::endl;
420   os << "Number of finite elements = " << nElements_ << " (" << pruned_ << " pruned)"
421      << std::endl;
422   os << "Number of irreducible finite elements = " << nIrrElements_ << std::endl;
423   os << "============ Spheres list (in Angstrom)" << std::endl;
424   os << " Sphere   on   Radius   Alpha       X            Y            Z     \n";
425   os << "-------- ---- -------- ------- -----------  -----------  -----------\n";
426   // Print original set of spheres
427   int original = nSpheres_ - addedSpheres;
428   Eigen::IOFormat CleanFmt(6, Eigen::DontAlignCols, "     ", "\n", "", "");
429   for (int i = 0; i < original; ++i) {
430     os << std::setw(4) << i + 1;
431     os << "      " << molecule_.atoms()[i].symbol << "    ";
432     os << std::fixed << std::setprecision(4)
433        << molecule_.spheres()[i].radius * bohrToAngstrom();
434     os << std::fixed << std::setprecision(2) << "   "
435        << molecule_.atoms()[i].radiusScaling << "     ";
436     os << (molecule_.spheres()[i].center.transpose() * bohrToAngstrom())
437               .format(CleanFmt);
438     os << std::endl;
439   }
440   // Print added spheres
441   for (int j = 0; j < addedSpheres; ++j) {
442     int idx = original + j;
443     os << std::setw(4) << idx + 1;
444     os << "      Du    ";
445     os << std::fixed << std::setprecision(4)
446        << sphereRadius_(idx) * bohrToAngstrom();
447     os << std::fixed << std::setprecision(2) << "   1.00";
448     os << (sphereCenter_.col(idx).transpose() * bohrToAngstrom()).format(CleanFmt);
449     os << std::endl;
450   }
451   return os;
452 }
453 
createGePolCavity(const CavityData & data)454 ICavity * createGePolCavity(const CavityData & data) {
455   return new GePolCavity(
456       data.molecule, data.area, data.probeRadius, data.minimalRadius);
457 }
458 } // namespace cavity
459 } // namespace pcm
460