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