1 /******************************************************************************
2 
3   This source file is part of the Avogadro project.
4 
5   Copyright 2012-2013 Kitware, Inc.
6   Copyright 2018 Geoffrey Hutchison
7 
8   This source code is released under the New BSD License, (the "License").
9 
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15 
16 ******************************************************************************/
17 #include "surfaces.h"
18 #include "surfacedialog.h"
19 
20 #include "gaussiansetconcurrent.h"
21 #include "slatersetconcurrent.h"
22 
23 // Header only, but duplicate symbols if included globally...
24 namespace {
25 #include <gif.h>
26 }
27 
28 #include <gwavi.h>
29 
30 #include <avogadro/core/variant.h>
31 
32 #include <avogadro/core/cube.h>
33 #include <avogadro/core/mesh.h>
34 #include <avogadro/qtgui/meshgenerator.h>
35 #include <avogadro/qtgui/molecule.h>
36 #include <avogadro/qtopengl/activeobjects.h>
37 #include <avogadro/qtopengl/glwidget.h>
38 
39 #include <avogadro/core/basisset.h>
40 #include <avogadro/core/gaussiansettools.h>
41 
42 #include <avogadro/io/fileformatmanager.h>
43 #include <avogadro/quantumio/gamessus.h>
44 #include <avogadro/quantumio/gaussiancube.h>
45 #include <avogadro/quantumio/gaussianfchk.h>
46 #include <avogadro/quantumio/molden.h>
47 #include <avogadro/quantumio/mopacaux.h>
48 #include <avogadro/quantumio/nwchemjson.h>
49 #include <avogadro/quantumio/nwchemlog.h>
50 
51 #include <QtCore/QBuffer>
52 #include <QtCore/QCoreApplication>
53 #include <QtCore/QDebug>
54 #include <QtCore/QProcess>
55 #include <QtGui/QOpenGLFramebufferObject>
56 #include <QtWidgets/QAction>
57 #include <QtWidgets/QFileDialog>
58 #include <QtWidgets/QMessageBox>
59 #include <QtWidgets/QProgressDialog>
60 
61 namespace Avogadro {
62 namespace QtPlugins {
63 
64 using Core::Cube;
65 using Core::GaussianSet;
66 using QtGui::Molecule;
67 
68 class Surfaces::PIMPL
69 {
70 public:
71   GifWriter* gifWriter = nullptr;
72   gwavi_t* gwaviWriter = nullptr;
73 };
74 
Surfaces(QObject * p)75 Surfaces::Surfaces(QObject* p) : ExtensionPlugin(p), d(new PIMPL())
76 {
77   auto action = new QAction(this);
78   action->setText(tr("Create Surfaces..."));
79   connect(action, SIGNAL(triggered()), SLOT(surfacesActivated()));
80   m_actions.push_back(action);
81 
82   // Register quantum file formats
83   Io::FileFormatManager::registerFormat(new QuantumIO::GAMESSUSOutput);
84   Io::FileFormatManager::registerFormat(new QuantumIO::GaussianFchk);
85   Io::FileFormatManager::registerFormat(new QuantumIO::GaussianCube);
86   Io::FileFormatManager::registerFormat(new QuantumIO::MoldenFile);
87   Io::FileFormatManager::registerFormat(new QuantumIO::MopacAux);
88   Io::FileFormatManager::registerFormat(new QuantumIO::NWChemJson);
89   Io::FileFormatManager::registerFormat(new QuantumIO::NWChemLog);
90 }
91 
~Surfaces()92 Surfaces::~Surfaces()
93 {
94   delete d;
95   delete m_cube;
96 }
97 
setMolecule(QtGui::Molecule * mol)98 void Surfaces::setMolecule(QtGui::Molecule* mol)
99 {
100   if (mol->basisSet()) {
101     m_basis = mol->basisSet();
102   } else if (mol->cubes().size() != 0) {
103     m_cubes = mol->cubes();
104   }
105 
106   m_cube = nullptr;
107   m_mesh1 = nullptr;
108   m_mesh2 = nullptr;
109   m_molecule = mol;
110 }
111 
actions() const112 QList<QAction*> Surfaces::actions() const
113 {
114   return m_actions;
115 }
116 
menuPath(QAction *) const117 QStringList Surfaces::menuPath(QAction*) const
118 {
119   QStringList path;
120   path << tr("&Analysis");
121   return path;
122 }
123 
surfacesActivated()124 void Surfaces::surfacesActivated()
125 {
126   if (!m_dialog) {
127     m_dialog = new SurfaceDialog(qobject_cast<QWidget*>(parent()));
128     connect(m_dialog, SIGNAL(calculateClickedSignal()),
129             SLOT(calculateSurface()));
130     connect(m_dialog, SIGNAL(recordClicked()), SLOT(recordMovie()));
131     connect(m_dialog, SIGNAL(stepChanged(int)), SLOT(stepChanged(int)));
132   }
133 
134   if (m_basis) {
135     // we have quantum data, set up the dialog accordingly
136     auto gaussian = dynamic_cast<Core::GaussianSet*>(m_basis);
137     bool beta = false;
138     if (gaussian) {
139       auto b = gaussian->moMatrix(GaussianSet::Beta);
140       if (b.rows() > 0 && b.cols() > 0)
141         beta = true;
142     }
143     m_dialog->setupBasis(m_basis->electronCount(),
144                          m_basis->molecularOrbitalCount(), beta);
145   }
146   if (m_cubes.size() > 0) {
147     QStringList cubeNames;
148     for (unsigned int i = 0; i < m_cubes.size(); ++i) {
149       cubeNames << m_cubes[i]->name().c_str();
150     }
151     m_dialog->setupCubes(cubeNames);
152   }
153   m_dialog->setupSteps(m_molecule->coordinate3dCount());
154 
155   m_dialog->show();
156 }
157 
calculateSurface()158 void Surfaces::calculateSurface()
159 {
160   if (!m_dialog)
161     return;
162 
163   Type type = m_dialog->surfaceType();
164   if (!m_cube)
165     m_cube = m_molecule->addCube();
166   // TODO we should add a name, type, etc.
167 
168   switch (type) {
169     case VanDerWaals:
170     case SolventAccessible:
171     case SolventExcluded:
172       calculateEDT();
173       // pass a molecule and return a Cube for m_cube
174       //   displayMesh();
175       break;
176 
177     case ElectronDensity:
178     case MolecularOrbital:
179     case ElectrostaticPotential:
180     case SpinDensity:
181       calculateQM();
182       break;
183 
184     case FromFile:
185     default:
186       calculateCube();
187       break;
188   }
189 }
190 
calculateEDT()191 void Surfaces::calculateEDT()
192 {
193   // pass the molecule to the EDT, plus the surface type
194   // get back a Cube object in m_cube
195 }
196 
calculateQM()197 void Surfaces::calculateQM()
198 {
199   if (!m_basis || !m_dialog)
200     return; // nothing to do
201 
202   // Reset state a little more frequently, minimal cost, avoid bugs.
203   m_molecule->clearCubes();
204   m_molecule->clearMeshes();
205   m_cube = nullptr;
206   m_mesh1 = nullptr;
207   m_mesh2 = nullptr;
208   m_molecule->emitChanged(Molecule::Atoms | Molecule::Added);
209   bool connectSlots = false;
210 
211   // set up QtConcurrent calculators for Gaussian or Slater basis sets
212   if (dynamic_cast<GaussianSet*>(m_basis)) {
213     if (!m_gaussianConcurrent) {
214       m_gaussianConcurrent = new GaussianSetConcurrent(this);
215       connectSlots = true;
216     }
217     m_gaussianConcurrent->setMolecule(m_molecule);
218   } else {
219     if (!m_slaterConcurrent) {
220       m_slaterConcurrent = new SlaterSetConcurrent(this);
221       connectSlots = true;
222     }
223     m_slaterConcurrent->setMolecule(m_molecule);
224   }
225 
226   // TODO: Check to see if this cube or surface has already been computed
227   if (!m_progressDialog) {
228     m_progressDialog = new QProgressDialog(qobject_cast<QWidget*>(parent()));
229     m_progressDialog->setCancelButtonText(nullptr);
230     m_progressDialog->setWindowModality(Qt::NonModal);
231     connectSlots = true;
232   }
233 
234   if (!m_cube)
235     m_cube = m_molecule->addCube();
236 
237   Type type = m_dialog->surfaceType();
238   int index = m_dialog->surfaceIndex();
239   m_isoValue = m_dialog->isosurfaceValue();
240   m_cube->setLimits(*m_molecule, m_dialog->resolution(), 5.0);
241 
242   QString progressText;
243   if (type == ElectronDensity) {
244     progressText = tr("Calculating electron density");
245     m_cube->setName("Electron Denisty");
246     if (dynamic_cast<GaussianSet*>(m_basis)) {
247       m_gaussianConcurrent->calculateElectronDensity(m_cube);
248     } else {
249       m_slaterConcurrent->calculateElectronDensity(m_cube);
250     }
251   }
252 
253   else if (type == MolecularOrbital) {
254     progressText = tr("Calculating molecular orbital %L1").arg(index);
255     m_cube->setName("Molecular Orbital " + std::to_string(index + 1));
256     if (dynamic_cast<GaussianSet*>(m_basis)) {
257       m_gaussianConcurrent->calculateMolecularOrbital(m_cube, index,
258                                                       m_dialog->beta());
259     } else {
260       m_slaterConcurrent->calculateMolecularOrbital(m_cube, index);
261     }
262   }
263 
264   // Set up the progress dialog.
265   if (dynamic_cast<GaussianSet*>(m_basis)) {
266     m_progressDialog->setWindowTitle(progressText);
267     m_progressDialog->setRange(
268       m_gaussianConcurrent->watcher().progressMinimum(),
269       m_gaussianConcurrent->watcher().progressMaximum());
270     m_progressDialog->setValue(m_gaussianConcurrent->watcher().progressValue());
271     m_progressDialog->show();
272 
273     if (connectSlots) {
274       connect(&m_gaussianConcurrent->watcher(),
275               SIGNAL(progressValueChanged(int)), m_progressDialog,
276               SLOT(setValue(int)));
277       connect(&m_gaussianConcurrent->watcher(),
278               SIGNAL(progressRangeChanged(int, int)), m_progressDialog,
279               SLOT(setRange(int, int)));
280       connect(m_gaussianConcurrent, SIGNAL(finished()), SLOT(displayMesh()));
281     }
282   } else {
283     // slaters
284     m_progressDialog->setWindowTitle(progressText);
285     m_progressDialog->setRange(m_slaterConcurrent->watcher().progressMinimum(),
286                                m_slaterConcurrent->watcher().progressMaximum());
287     m_progressDialog->setValue(m_slaterConcurrent->watcher().progressValue());
288     m_progressDialog->show();
289 
290     connect(&m_slaterConcurrent->watcher(), SIGNAL(progressValueChanged(int)),
291             m_progressDialog, SLOT(setValue(int)));
292     connect(&m_slaterConcurrent->watcher(),
293             SIGNAL(progressRangeChanged(int, int)), m_progressDialog,
294             SLOT(setRange(int, int)));
295     connect(m_slaterConcurrent, SIGNAL(finished()), SLOT(displayMesh()));
296   }
297 }
298 
calculateCube()299 void Surfaces::calculateCube()
300 {
301   if (!m_dialog || m_cubes.size() == 0)
302     return;
303 
304   // check bounds
305   m_cube = m_cubes[m_dialog->surfaceIndex()];
306   m_isoValue = m_dialog->isosurfaceValue();
307   displayMesh();
308 }
309 
stepChanged(int n)310 void Surfaces::stepChanged(int n)
311 {
312   if (!m_molecule || !m_basis)
313     return;
314 
315   qDebug() << "\n\t==== Step changed to" << n << "====";
316   auto g = dynamic_cast<GaussianSet*>(m_basis);
317   if (g) {
318     g->setActiveSetStep(n - 1);
319     m_molecule->clearCubes();
320     m_molecule->clearMeshes();
321     m_cube = nullptr;
322     m_mesh1 = nullptr;
323     m_mesh2 = nullptr;
324     m_molecule->emitChanged(Molecule::Atoms | Molecule::Added);
325   }
326 }
327 
displayMesh()328 void Surfaces::displayMesh()
329 {
330   if (!m_cube)
331     return;
332 
333   if (!m_mesh1)
334     m_mesh1 = m_molecule->addMesh();
335   if (!m_meshGenerator1) {
336     m_meshGenerator1 = new QtGui::MeshGenerator;
337     connect(m_meshGenerator1, SIGNAL(finished()), SLOT(meshFinished()));
338   }
339   m_meshGenerator1->initialize(m_cube, m_mesh1, -m_isoValue);
340 
341   // TODO - only do this if we're generating an orbital
342   //    and we need two meshes
343   //   How do we know? - likely ask the cube if it's an MO?
344   if (!m_mesh2)
345     m_mesh2 = m_molecule->addMesh();
346   if (!m_meshGenerator2) {
347     m_meshGenerator2 = new QtGui::MeshGenerator;
348     connect(m_meshGenerator2, SIGNAL(finished()), SLOT(meshFinished()));
349   }
350   m_meshGenerator2->initialize(m_cube, m_mesh2, m_isoValue, true);
351 
352   // Start the mesh generation - this needs an improved mutex with a read lock
353   // to function as expected. Write locks are exclusive, read locks can have
354   // many read locks but no write lock.
355   m_meshGenerator1->start();
356   m_meshGenerator2->start();
357 
358   // Track how many meshes are left to show.
359   m_meshesLeft = 2;
360 }
361 
meshFinished()362 void Surfaces::meshFinished()
363 {
364   --m_meshesLeft;
365   if (m_meshesLeft == 0) {
366     if (m_recordingMovie) {
367       // Move to the next frame.
368       qDebug() << "Let's get to the next frame...";
369       m_molecule->emitChanged(QtGui::Molecule::Added);
370       movieFrame();
371     } else {
372       m_dialog->reenableCalculateButton();
373       m_molecule->emitChanged(QtGui::Molecule::Added);
374     }
375   }
376   // TODO: enable the mesh display type
377 }
378 
recordMovie()379 void Surfaces::recordMovie()
380 {
381   QString baseFileName;
382   if (m_molecule)
383     baseFileName = m_molecule->data("fileName").toString().c_str();
384 
385   QString selectedFilter = tr("Movie AVI (*.avi)");
386   QString baseName = QFileDialog::getSaveFileName(
387     qobject_cast<QWidget*>(parent()), tr("Export Movie"), "",
388     tr("Movie MP4 (*.mp4);;Movie AVI (*.avi);;GIF (*.gif)"), &selectedFilter);
389 
390   if (baseName.isEmpty()) {
391     m_dialog->enableRecord();
392     return;
393   }
394 
395   QFileInfo fileInfo(baseName);
396   if (!fileInfo.suffix().isEmpty())
397     baseName = fileInfo.absolutePath() + "/" + fileInfo.baseName();
398 
399   m_baseFileName = baseName;
400   m_numberLength = static_cast<int>(
401     ceil(log10(static_cast<float>(m_molecule->coordinate3dCount()) + 1)));
402 
403   m_recordingMovie = true;
404   m_currentFrame = 1;
405   m_frameCount = m_molecule->coordinate3dCount();
406 
407   // Figure out the save type, and work accordingly...
408   if (selectedFilter == tr("GIF (*.gif)")) {
409     d->gwaviWriter = nullptr;
410     d->gifWriter = new GifWriter;
411     GifBegin(d->gifWriter, (baseName + ".gif").toLatin1().data(), 800, 600,
412              100 / 4);
413   } else if (selectedFilter == tr("Movie AVI (*.avi)")) {
414     d->gifWriter = nullptr;
415     d->gwaviWriter = gwavi_open((baseName + ".avi").toLatin1().data(), 800, 600,
416                                 "MJPG", 4, NULL);
417   } else {
418     d->gwaviWriter = nullptr;
419     d->gifWriter = nullptr;
420   }
421 
422   stepChanged(m_currentFrame);
423   m_dialog->setStep(m_currentFrame);
424   calculateSurface();
425 }
426 
movieFrame()427 void Surfaces::movieFrame()
428 {
429   // Not ideal, need to let things update asynchronously, complete, before we
430   // capture the frame. When appropriate move to the next frame or complete.
431   QCoreApplication::sendPostedEvents();
432   QCoreApplication::processEvents();
433 
434   auto glWidget = QtOpenGL::ActiveObjects::instance().activeGLWidget();
435   if (!glWidget) {
436     QMessageBox::warning(qobject_cast<QWidget*>(parent()), tr("Avogadro"),
437                          "Couldn't find the active render widget, failing.");
438     m_recordingMovie = false;
439     m_dialog->enableRecord();
440     return;
441   }
442   glWidget->resize(800 / glWidget->devicePixelRatio(),
443                    600 / glWidget->devicePixelRatio());
444   QImage exportImage;
445   glWidget->raise();
446   glWidget->repaint();
447   if (QOpenGLFramebufferObject::hasOpenGLFramebufferObjects()) {
448     exportImage = glWidget->grabFramebuffer();
449   } else {
450     QPixmap pixmap = QPixmap::grabWindow(glWidget->winId());
451     exportImage = pixmap.toImage();
452   }
453 
454   if (d->gifWriter) {
455     int pixelCount = exportImage.width() * exportImage.height();
456     uint8_t* imageData = new uint8_t[pixelCount * 4];
457     int imageIndex = 0;
458     for (int j = 0; j < exportImage.height(); ++j) {
459       for (int k = 0; k < exportImage.width(); ++k) {
460         QColor color = exportImage.pixel(k, j);
461         imageData[imageIndex] = static_cast<uint8_t>(color.red());
462         imageData[imageIndex + 1] = static_cast<uint8_t>(color.green());
463         imageData[imageIndex + 2] = static_cast<uint8_t>(color.blue());
464         imageData[imageIndex + 3] = static_cast<uint8_t>(color.alpha());
465         imageIndex += 4;
466       }
467     }
468     GifWriteFrame(d->gifWriter, imageData, 800, 600, 100 / 4);
469     delete[] imageData;
470   } else if (d->gwaviWriter) {
471     QByteArray ba;
472     QBuffer buffer(&ba);
473     buffer.open(QIODevice::WriteOnly);
474     exportImage.save(&buffer, "JPG");
475     if (gwavi_add_frame(
476           d->gwaviWriter,
477           reinterpret_cast<const unsigned char*>(buffer.data().data()),
478           buffer.size()) == -1) {
479       QMessageBox::warning(qobject_cast<QWidget*>(parent()), tr("Avogadro"),
480                            tr("Error: cannot add frame to video."));
481     }
482   } else {
483     QString fileName = QString::number(m_currentFrame);
484     while (fileName.length() < m_numberLength)
485       fileName.prepend('0');
486     fileName.prepend(m_baseFileName);
487     fileName.append(".png");
488     qDebug() << "Writing to" << fileName;
489 
490     if (!exportImage.save(fileName)) {
491       QMessageBox::warning(qobject_cast<QWidget*>(parent()), tr("Avogadro"),
492                            tr("Cannot save file %1.").arg(fileName));
493       return;
494     }
495   }
496 
497   // Increment current frame.
498   ++m_currentFrame;
499   if (m_currentFrame <= m_frameCount) {
500     qDebug() << "Starting next frame...";
501     stepChanged(m_currentFrame);
502     m_dialog->setStep(m_currentFrame);
503     calculateSurface();
504   } else {
505     qDebug() << "We are done! Make some movies.";
506     if (d->gifWriter) {
507       GifEnd(d->gifWriter);
508       delete d->gifWriter;
509       d->gifWriter = nullptr;
510     } else if (d->gwaviWriter) {
511       gwavi_close(d->gwaviWriter);
512       d->gwaviWriter = nullptr;
513     } else {
514       QProcess proc;
515       QStringList args;
516       args << "-y"
517            << "-r" << QString::number(10) << "-i"
518            << m_baseFileName + "%0" + QString::number(m_numberLength) + "d.png"
519            << "-c:v"
520            << "libx264"
521            << "-r"
522            << "30"
523            << "-pix_fmt"
524            << "yuv420p" << m_baseFileName + ".mp4";
525       proc.execute("avconv", args);
526     }
527     /*
528     args.clear();
529     args << "-dispose"
530          << "Background"
531          << "-delay" << QString::number(100 / 10)
532          << m_baseFileName + "%0" + QString::number(m_numberLength) + "d.png[0-"
533     +
534               QString::number(m_molecule->coordinate3dCount() - 1) + "]"
535          << m_baseFileName + ".gif";
536     proc.execute("convert", args);
537     */
538 
539     m_recordingMovie = false;
540     m_dialog->enableRecord();
541   }
542 }
543 
544 } // namespace QtPlugins
545 } // namespace Avogadro
546