A library for exploring persistent homology
In contrast to other libraries for topological data analysis, Aleph is
built around a set of classes that facilitate the description of
topological objects. The most relevant classes in this context are
Simplex
and SimplicialComplex
. In practice, you probably never have
to build your own simplicial complex fully from scratch—we build
one here in order to illustrate the concepts.
The Simplex
class models a simplex of arbitrary dimensions that
contains a weight, or—more generically—a data value, and
a list of vertices. The class is templated to support different choice
for both the data type and the vertex type. Let us start by building
some simplices:
using namespace aleph::topology;
Simplex s1( {1}, 1 );
Simplex s2( {2}, 2 );
Simplex s21( {2,1}, 3 );
// Output: 1,2,3
std::cout << s1.data() << "," << s2.data() << "," << s21.data() << "\n";
// Data can also be changed
s21.setData( 42 );
The Simplex
class offers some iterators as well. Vertices will be
traversed in reverse order. Faces will be traversed in lexicographical
order:
// Output:
// - 2
// - 1
for( auto&& vertex : s21 )
std::cout << "- " << vertex << "\n";
// Output:
// - {1}
// - {2}
for( auto it = s21.begin_boundary(); it != s21.end_boundary(); ++it )
std::cout << "- " << *it << "\n";
As you can see from this example, the Simplex
class also supports
output operations. Simplices will be denoted by curly braces, with the
data value only being shown if it is non-zero. Note that the boundary
iterator is incapable of displaying weights because only the weight for
the co-face, i.e. the original simplex, has been specified.
There are all sorts of other queries for simplices:
std::cout << s1.dimension() << "\n"; // 0
std::cout << s2.dimension() << "\n"; // 0
std::cout << s21.dimension() << "\n"; // 1
std::cout << s21.size() << "\n"; // 2 (number of vertices)
std::cout << s21.empty() << "\n"; // false
std::cout << s21 == s1 << "\n"; // false
std::cout << s1 < s21 << "\n"; // true (lexicographical ordering)
std::cout << s21[0] << "\n"; // 2
std::cout << s21[1] << "\n"; // 1
std::cout << s21[2] << "\n"; // exception
Simplices are also convertible to bool
, which facilitates their use in
some algorithms. For example, the following is a valid query:
void f( const Simplex& s )
{
if( s )
std::cout << "This simplex is non-empty!\n";
}
Please refer to the source code or the API documentation for more information.
The SimplicialComplex
class models the concept of a simplicial
complex. It consists of a range of simplices that may be accessed using
different views. Most of the functionality is provided for
convenience purposes only—in theory, it would often be sufficient
to just provide an std::vector
of simplices. However, the
SimplicialComplex
class is easier to use and provides faster access to
individual simplices. Let us first build a simplicial complex that
models a triangle:
using namespace aleph::topology;
std::vector<Simplex> simplices
= { {1}, {2}, {4}, {2,4}, {1,2}, {1,4} };
SimplicialComplex K( simplices.begin(), simplices.end() );
Note that the vertex indices are not contiguous. K
does not use vertex
indices directly, which makes it possible to store simplicial
sub-complexes and arbitrary subsets of complexes. The downside of this
approach is that the class cannot guarantee that it actually represents
a valid simplicial complexes. It is possible that certain faces are
missing from the complex. Duplicates, on the other hand, will not be
allowed automatically.
We may now iterate over K
in numerous ways:
// Output follows the insertion order from above. This iterator is the
// default iterator for representing a filtration.
for( auto&& simplex : K )
std::cout << simplex << "\n";
// Output follows increasing dimensionality of simplices. This does not
// deviate from the filtration order in this example.
for( auto it = K.begin_dimension(); it != K.end_dimension(); ++it )
std::cout << *it << "\n";
// Output follows lexicographical ordering:
// {1}
// {2}, {2,1}
// {4}, {4,1}, {4,2}
for( auto it = K.begin_lexicographical(); it != K.end_lexicographical(); ++it )
std::cout << *it << "\n";
Note that these iterators are read-only by necessity. If you want to
replace a simplex, use the replace()
function instead:
for( auto it = K.begin(); it != K.end(); ++it )
{
auto s = *it;
s.setData( 42 );
if( !K.replace( it, s ) )
{
// Simplex could not be replaced for some reason...
}
}
Again, there are numerous query functions:
std::cout << K.contains( {1,2} ) << "\n"; // true
std::cout << K.contains( {0,2} ) << "\n"; // false
std::cout << K.index( {1} ) << "\n"; // 0, because it is the first inserted simplex
std::cout << K.at( 3 ) << "\n"; // {2,4}
std::cout << K.at( 42 ) << "\n"; // exception
std::cout << K.size() << "\n"; // 5
std::cout << K.empty() << "\n"; // false
std::cout << K.dimension() << "\n"; // 1, because we have at most 1-dimensional simplices
Finally, simplicial complexes can also be sorted. Here, we only take a look at basic inline sorting predicates. When calculating persistent homology, we will use predicates that are more involved:
K.sort( [] ( const Simplex& s, const Simplex& t )
{
return s < t;
}
);
This sorts the complex according to the lexicographical ordering induced by the simplices.
The SimplicialComplex
class is central to many calculations and
operations of Aleph. Many real-world objects, such as meshes or graphs,
can be represented as a simplicial complex. To this end, Aleph offers
several I/O classes.
We cover them in another tutorial.
More information about the SimplicialComplex
class is available in the
source
code
or the API documentation of Aleph. We will also use simplicial
complexes when calculating persistent homology.