Topology ToolKit


Tutorials

Writing a TTK module:
An introduction with Betti numbers


The purpose of this exercise tutorial is threefold:
   ·  Introduce you to the usage of TTK in ParaView;
   ·  Introduce you to the development of a TTK module;
   ·  Make you learn more about the homology groups of 3-manifolds in R3!

This exercise requires some minor background in C++.
It should take no more than 2 hours of your time.

1. Downloads

In the following, we will assume that TTK has been installed successfully on your system. If not, please visit our installation page for detailed instructions HERE.
Before starting the exercise, please download the data package HERE as well as the latest TTK source tree THERE.
Move the tarballs to a working directory (for instance called ~/ttk) and decompress them by entering the following commands (omit the $ character) in a terminal (this assumes that you downloaded the tarballs to the ~/Downloads directory):

$ mkdir ~/ttk
$ mv ~/Downloads/ttk-0.9.2.tar.gz ~/ttk/
$ mv ~/Downloads/bettiData.tar.gz ~/ttk/
$ cd ~/ttk
$ tar xvzf ttk-0.9.2.tar.gz
$ tar xvzf bettiData.tar.gz

You can delete the tarballs after the source trees have been decompressed by entering the following commands:

$ rm ttk-0.9.2.tar.gz
$ rm bettiData.tar.gz

2. TTK/ParaView 101

ParaView is the leading application for the interactive analysis and visualization of scientific data. It is open-source (BSD license). It is developed by Kitware, a prominent company in open-source software (CMake, CDash, VTK, ITK, etc.).

ParaView is a graphical user interface to the Visualization ToolKit, a C++ library for data visualization and analysis, also developed in open-source by Kitware.
VTK and ParaView both implement a pipeline model, where the data to analyze and visualize is passed on the input of a (possibly complex) sequence of elementary processing units, called filters. Each filter is implemented by a single C++ class in VTK. In ParaView, users can design advanced processing pipelines by placing manually each filter in their pipelines.
In the above example, the active pipeline (shown in the "Pipeline Browser", upper left panel) can be interpreted as a series of processing instructions and reads similarly to some source code: first, the input PL 3-manifold is clipped (Clip1); second, the boundary of the input PL 3-manifold is extracted (ExtractSurface1), third the connected components of the boundary are isolated and clipped with the same parameters as the rest of the volume (Clip2).

The output of each filter can be visualized independently (by toggling the eye icon, left column). The algorithmic parameters of each filter can be tuned in the "Properties" panel (bottom left panel) by selecting a filter in the pipeline (in the above example GenerateSurfaceNormals1). The display properties of the output of a filter in the main central view can also be modified from this panel.
To create a pipeline from scratch, users typically load their input data and apply successively the required filters, by browsing the filter menu (exhaustive list shown above), where the filters which are not compatible with the object currently selected in the pipeline are shaded.
Alternatively to the filter menu, users can toggle a fast search dialog by pressing the Ctrl+space keystroke (under Linux) and enter keywords as shown above to quickly call filters.

Once users are satisfied with their analysis and visualization pipeline, they can save it to disk for later re-use in the form of a Python script with the menu File, Save state... and choosing the Python state file type. In the Python script, each filter instance is modeled by an independent Python object, for which attributes can be changed and functions can be called.
Note that the output Python script can be run independently of ParaView (for instance in batch mode) and its content can be included in any Python code (the pvpython interpreter is then recommended). Thus, ParaView can be viewed as an interactive designer of advanced analysis Python programs. Then, writing up your own TTK module will not only expose your C++ code to ParaView, but it will also make it readily available in Python. To learn more about the available ParaView filters, please see the following tutorials: HERE and THERE.

Exercise 1

Open the skull data set in ParaView with the following command (omit the $ character):

$ paraview ~/ttk/bettiData/skull.vtu

Next, try to reproduce the visualization shown in the first screenshot above, which shows a clipped view of the PL 3-manifold, with the visible interior triangles in white with dark edges, each boundary component displayed with its edges with a distinct color and the rest of the (unclipped) boundary shown in transparent.

3. Betti numbers of PL 3-manifolds

In the following, you will write your first TTK module, dedicated to the computation of Betti numbers of PL 3-manifolds. For this, we will exploit the results by Dey and Guha, published in their 1998 paper "Computing Homology Groups of Simplicial Complexes in R3".

Go ahead and create your TTK module by entering the following commands (omit the $ character):

$ cd ~/ttk/ttk-0.9.2/
$ scripts/createTTKmodule.sh BettiNumbers

The above script will create all the files and classes of your TTK module. This module includes automatically generated standalone command line and GUI programs, as well as a ParaView plugin.

For this exercise, we will only need to build your TTK module, and not the entire TTK collection. Thus, edit the file ~/ttk/ttk-0.9.2/CMakeLists.txt to remove all entries, except the first line and all the lines containing the word BettiNumbers.
Now, to build your TTK module, enter the following commands, where N is the number of available cores on your system (omit the $ character):

$ cd ~/ttk/ttk-0.9.2/
$ mkdir build
$ cd build
$ cmake ../
$ make -jN

Note that, in order to rebuild your module in the future, you'll only need to enter the last command (make -jN).
To load your module in ParaView, open ParaView, go to the menu Tools, Manage Plugins..., then click on the Load New... button. Next, from the file browser dialog, select the library we've just built: ~/ttk/ttk-0.9.2/build/paraview/BettiNumbers/libBettiNumbers.so. Next, you may want to check the checkbox AutoLoad as shown above (available after expending your plugin entry) to force ParaView to automatically load your plugin at startup.
At this point, if your plugin was successfully loaded, it should appear as the entry TTK BettiNumbers under the menu Filters then TTK - Misc, as shown below.
Now open the file ~/ttk/ttk-0.9.2/core/baseCode/bettiNumbers/BettiNumbers.h. In the function execute(), add the line cout << endl << "Hello World!" << endl << endl; before the return statement. Now build your module again, load the skull data set in ParaView and apply your BettiNumbers plugin. You should see your message "Hello World!" appear in the terminal, as shown below.
If this is the case, congratulations! You've just written your first TTK module!

Exercise 2: B0 (~20 lines of code)

The 0-th Betti number (noted B0) corresponds to the number of connected component of the input manifold M. In the following, we will compute this number and display it in the output of the terminal (next to our "Hello World!" message).

Several algorithms exist to enumerate connected components in a triangulation, the simplest being a simple breadth-first search traversal. Instead, we will use a Union-Find data-structure, which will make the implementation even simpler.

The Union-Find (UF) data-structure is a simple, yet powerful data-structure which tracks the connectivity of a graph over successive edge additions. It is a fundamental object which plays a key role in many computational topology algorithms.

Initially, a UF object is created for each vertex of the graph. The operation find() on this object returns its parent, that is a unique representative UF object which represents the connected component of the queried vertex.

When adding an edge between two vertices v0 and v1, the makeUnion() operation is called between the UF objects of v0 and v1. This will have the effect of merging the representatives of the corresponding connected components.

Thus, to enumerate the connected components of a PL manifold M, one just needs to proceed as follows. First, one should iterate over the edges of M, call the makeUnion() operation between the UF objects of the vertices of each edge of M. Finally, one should iterate over all the UF objects, call the find() operation and enumerate the number of unique remaining representatives. This number corresponds to the number of remaining connected components after all edges have been added (initially, the number of connected components is equal to the number of vertices in M).

TTK provides a reference implementation of the Union-Find data structure (with path compression and union by rank), which we will use in this exercise. Its online documentation can be found HERE.

To use it in your module, edit the file ~/ttk/ttk-0.9.2/core/baseCode/bettiNumbers/bettiNumbers.cmake and add the line ttk_add_baseCode_package(unionFind) at the top of the file. Next, in ~/ttk/ttk-0.9.2/core/baseCode/bettiNumbers/BettiNumbers.h, add the line #include <UnionFind.h> in the file preamble.

TTK provides an efficient data structure for fast triangulation traversal. Its online documentation can be found HERE.

In ~/ttk/ttk-0.9.2/core/baseCode/bettiNumbers/BettiNumbers.h, a pointer to an object of this class (triangulation_) is readily available and correctly initialized to the triangulation passed on the input of your module.

TTK's triangulation data-structure supports a cache based access. This means the data-structure needs to be pre-conditioned depending on the type of traversal it will undergo, before it gets actually traversed. For instance, if you want to access the i-th vertex of the j-th edge of the triangulation (with the function getEdgeVertex()), you will need, before the actual traversal, to call the pre-conditioning function preprocessEdges() in a pre-process, as explained in the documentation of the getEdgeVertex() function. Note that each traversal function comes with its own pre-conditioning function, to be called in the pre-processing stage of your module.

Typically, in ~/ttk/ttk-0.9.2/core/baseCode/bettiNumbers/BettiNumbers.h, the function setupTriangulation() is the right placeholder to put such a pre-conditioning statement, while the actual traversal should happen in the execute() function, next to our prior "Hello World!" message.

Now go ahead and compute B0, the number of connected components of M.

To do this, you'll need to allocate an array (typically an STL vector) of UnionFind objects with as many entries as vertices in M. Next, you'll need to iterate over the edges of M and call the makeUnion() operation on the UF objects of each edge. Finally, you'll need to iterate over your initial array of UF objects, re-assign to each entry the result of its find() operation (to obtain the latest representative) and enumerate the number of unique remaining representatives.

Exercise 3: B2 (~30 lines of code)

The 2-nd Betti number (noted B2) corresponds to the number of voids of the input manifold M. To compute B2, we will exploit one of the results by Dey and Guha, published in their 1998 paper "Computing Homology Groups of Simplicial Complexes in R3".

Parse the above paper and identify the expression of B2. From there, edit the function execute() of ~/ttk/ttk-0.9.2/core/baseCode/bettiNumbers/BettiNumbers.h to also compute and display B2.

Exercise 4: B1 (~20 lines of code)

The 1-st Betti number (noted B1) corresponds to the number of independent cycles of the input manifold M. To compute B1, we will exploit one of the results by Dey and Guha, published in their 1998 paper "Computing Homology Groups of Simplicial Complexes in R3".

Parse the above paper and identify the expression of B1. From there, edit the function execute() of ~/ttk/ttk-0.9.2/core/baseCode/bettiNumbers/BettiNumbers.h to also compute and display B1.

Exercise 5: Euler characteristic and beyond (~3 lines of code)

Compute and display the Euler characteristic X of M. How does it relate to the Betti numbers of M?
Now, in ParaView, use the Threshold filter on your input triangulation and modify the Maximum parameter (the upper bound). Since our test data sets come with an attached scalar field, this will have the effect of selecting all the tetrahedra of M with vertices having a default scalar value below the Maximum parameter.

Now use your module to compute the Betti numbers of this selection. Now adjust the Maximum parameter again and click on Apply. The Betti numbers should be updated automatically.

Keep on modifying the value Maximum. Where do the Betti numbers change? What do these configurations correspond to?

Check your intuition with the other data-sets available in the ~/ttk/bettiData/ directory. Note that for at.vti, you will need to call the filter Tetrahedralize prior to any other processing.

Now, instead of changing the Maximum parameter of the Threshold filter, change the Minimum parameter instead. What do you observe in terms of the changes of Betti numbers?