Integrating external programs

In this tutorial, we show how to call an external executable from SAMSON, in order to perform calculations based on selected atoms in the document.

 

Setting up the external executable

Implement the following program, and compile it to produce an executable called Barycenter:

#include <sstream>
#include <fstream>
#include <string>
#include <iostream>
 
int main() {
 
	unsigned int numberOfAtoms;
 
	// open the file
 
	std::ifstream input("input.txt");
 
	// read the number of atoms
 
	std::string line;
	std::getline(input, line);
	std::stringstream firstLineParser(line);
	firstLineParser >> numberOfAtoms;
 
	if (!numberOfAtoms) {
 
		std::ofstream output("output.txt");
		output << "No atoms" << std::endl;
		output.close();
		return 0;
 
	}
 
	// read the atoms coordinates and update the barycenter
 
	double x = 0.0;
	double y = 0.0;
	double z = 0.0;
 
	while (std::getline(input, line)) {
 
		std::stringstream lineParser(line);
 
		double currentX = 0.0;
		double currentY = 0.0;
		double currentZ = 0.0;
 
		lineParser >> currentX >> currentY >> currentZ;
 
		x += currentX;
		y += currentY;
		z += currentZ;
 
	}
 
	x /= (double)numberOfAtoms;
	y /= (double)numberOfAtoms;
	z /= (double)numberOfAtoms;
 
	std::ofstream output("output.txt");
	output << x << "A " << y << "A " << z << "A" << std::endl;
 
	return 0;
 
}

This programs opens a text file named input.txt containing atoms coordinates, and creates a file named output.txt that contains the coordinates of the barycenter of the atoms (or the string “No atoms” if the initial file did not contain any atom). Note that input coordinates are expected to be in angstroms, and that output coordinates are in angstroms as well.

 

Setting up the SAMSON Element and its interface

Use the SAMSON Element Generator to create a new SAMSON Element called Barycenter, containing an app called SEBarycenterApp.

 

Set up the interface (SEBarycenterAppGUI.ui) as follows:

 
with the following widget names:

 

Now, add a onCompute() slot to the interface:

 

Finally, connect the released() signal of the push button to this new slot:

 

Don’t forget to modify the getName function of the SEBarycenterGUI.cpp file in order to show a user-friendly name (e.g. “Barycenter”) in the app menu.

Executing the external executable

Since the actual barycenter calculations are going to be performed in an external executable, we could assume that it’s OK to implement the main functionality of the app in the interface class, i.e. in the file SEBarycenterGUI.cpp. However, if we later wanted to expose the functionality of our SAMSON Element to other SAMSON Elements (thanks to SAMSON’s introspection mechanism, explored in another tutorial), we would only be able to expose the app itself, and not its interface. As a result, even in this case, we still want to implement the core functionality in the app itself, and not in its interface.

Add the slot declaration to the SEBarycenterGUI.hpp file:

Now, add the slot definition to the SEBarycenterGUI.cpp file. This just calls the app, and update the label to show the result:

void SEBarycenterAppGUI::onCompute() {
 
	ui.labelResult->setText(QString("Result: ") + QString::fromStdString(getApp()->compute()));
}

Note that, to separate functionality from interface, the app is going to return a std::string instead of a QString.

Add the function declaration inside the SEBarycenterApp in the SEBarycenterApp.hpp header:

	std::string compute();

We are going to use QProcess to execute an external process, SAMSON to find atoms in the document, and fstream to manipulate files, so we add the following headers at the beginning of the SEBarycenterApp.cpp file:

#include <fstream>
#include <QProcess>
#include "SAMSON.hpp"

Now, add the function definition at the end of the the SEBarycenterApp.cpp file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
std::string SEBarycenterApp::compute() {
 
	// find atoms
 
	SBNodeIndexer atomIndexer;
	SAMSON::getActiveDocument()->getNodes(atomIndexer, SBNode::IsType(SBNode::Atom) && SBNode::IsSelected());
 
	// generate the input file
 
	std::ofstream input("C:\\input.txt");
	input << atomIndexer.size() << std::endl;
 
	SB_FOR(SBNode* node, atomIndexer) {
 
		SBAtom* atom = static_cast<SBAtom*>(node);
		SBPosition3 position = atom->getPosition();
 
		input << SBQuantity::angstrom(position.v[0]).getValue() << " ";
		input << SBQuantity::angstrom(position.v[1]).getValue() << " ";
		input << SBQuantity::angstrom(position.v[2]).getValue() << std::endl;
 
	}
 
	// execute the program and wait until it completes
 
	QString program("C:\\Barycenter.exe");
	QStringList arguments;
	QProcess* process = new QProcess();
	process->setWorkingDirectory("C:\\");
	process->start(program, arguments);
	process->waitForFinished();
	delete process;
 
	// read the output
 
	std::ifstream output("C:\\output.txt");
	std::string result;
	std::getline(output, result);
 
	return result;
 
}

Important: in a user-friendly app, it would be possible to automatically guess where the external executable is located, or to ask the user to locate it. Here, you need to adapt lines 10, 26, 29 and 36 based on where you placed the external executable Barycenter (and based on your OS, since exe assumes a Windows extension).

Lines 5 and 6 find selected atoms in the document. Lines 13 to 22 generate the input.txt file that is read by the external executable. Lines 26 to 32 run the external executable and wait for it to finish. Finally, lines 36 to 40 read the output of the external executable and returns its first line.

After compiling and installing (build the install target), you may now use the app:

 

A final note: in this tutorial, the external executable is expected to be pretty efficient, so that we wait for it to finish before continuing. In the general case, you would connect the finished signal of the process to a Qt slot. Please refer to the documentation of QProcess for more information.

Comments are closed.