Local congruence of chain complexes

03/31/2020 ∙ by Gianmaria DelMonte, et al. ∙ 0

The object of this paper is to transform a set of local chain complexes to a single global complex using an equivalence relation of congruence of cells, solving topologically the numerical inaccuracies of floating-point arithmetics. While computing the space arrangement generated by a collection of cellular complexes, one may start from independently and efficiently computing the intersection of each single input 2-cell with the others. The topology of these intersections is codified within a set of (0-2)-dimensional chain complexes. The target of this paper is to merge the local chains by using the equivalence relations of ϵ-congruence between 0-, 1-, and 2-cells (elementary chains). In particular, we reduce the block-diagonal coboundary matrices [Δ_0] and [Δ_1], used as matrix accumulators of the local coboundary chains, to the global matrices [δ_0] and [δ_1], representative of congruence topology, i.e., of congruence quotients between all 0-,1-,2-cells, via elementary algebraic operations on their columns. This algorithm is codified using the Julia porting of the SuiteSparse:GraphBLAS implementation of the GraphBLAS standard, conceived to efficiently compute algorithms on large graphs using linear algebra and sparse matrices [1, 2].



There are no comments yet.


page 1

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

Our goal in this paper was to develop and compare different implementations of a computational topology algorithm needed to solve efficiently and robustly the space decomposition (3D arrangement generated by polyhedral complexes) requested to evaluate Boolean formulas of Constructive Solid Geometry [2019arXiv191011848P].

Within the space decomposition pipeline to compute the arrangement [Halperin:2017] generated by a collection of cellular complexes [2017arXiv170400142P, 2019arXiv191108130P], the intermediate step is the assessment of the operator in 3D, after independent fragmentation of 2-faces in 2D, and before the topological gift wrapping (TGW) algorithm in 3D.

In particular, the (co)boundary chain is produced by merging a set of local coboundary matrices computed independently from each other on every fragmented input 2-cell. In paper [2019arXiv191108130P] this computational pipeline is discussed in full detail. In the present manuscript we provide the reduction of a chain complex to its “quotient topology” through congruence quotient computation.

We recall that the 2-skeleton of 3D arrangements, and its matrix, is computed by merging the local chain complexes produced by mutual intersections of 2-cells of input complexes. Each local chain complex , with , is generated by a two-dimensional input cell in the input collection, and by all 2-cells intersecting it. We call accumulator complex the set , . Two figures are geometrically congruent iff one can be transformed into the other by an isometry [2017arXiv170400142P]. The congruences between -cells of geometric complexes in are equivalence relations, so to compute the chain complex of quotient spaces by merging chains and operators by computing the graded quotient .

The merging is based on the discovering of all equivalence classes of the congruence relations between0-, 1-, and 2-cells of decomposed input objects, computed independently [2017arXiv170400142P] for each input 2-face. In order to discover all congruent pairs among decomposed -cells, topological tests are introduced through the computation of local topological invariants. Our Cell Congruence Enabling algorithm may be used to merge the topologies of several cellular complexes (even a large number), after having computed the intersections of their 2-cells. Two examples of actual application of the CCC algorithm are (a) the merging of plan drawings of city districts or buildings within a digital cartographic map, and (b) the mutual fragmentation of cell complexes before computing Boolean expressions [2019arXiv191011848P] between solid models.

The description of topology, geometry and physics of models using the chain of coboundary operators, i.e., of vector algebra operators, and their representation as sparse matrices, started with 

[PALMER1995733, Palmer1993], at least in the knowledge of the authors. While still unusual in the geometric and solid modeling areas (see [tuprints11291] for example), they seem currently to us the most promising approach to FEA methods [Desbrun:2006:DDF:1185657.1185665, Elcott:2006:BYO:1185657.1185666, arnold_falk_winther_2006, Arnold:2010, Arnold:2018, Tonti:1975, Tonti:2013, Ferretti:2014]. Also, the GraphBLAS standard, with linear algebra methods based on sparse matrices for fast algorithms on huge graphs, is currently having a big momentum [graphblas:20].

The computation of (co)boundary matrices from multiplication of sparse characteristic matrices of -complexes, was introduced in [Dicarlo:2014:TNL:2543138.2543294]. It may be useful to understand that the chain of (co)boundaries is an algebraic representation of the Hasse diagram of a cellular complex [10.1145/1236246.1236259, MILICCHIO2008172]. The main advantage of the matrix approach is that topology, geometry and physics may coexist in a unique sparse matrix framework, concurring together to define, represent and simulate the behavior of a model, without any restrictions on type, dimension, codimension, orientability, manifoldness, or connectedness [10.1145/1236246.1236259].

For the sake of reader convenience, the theoretical minimum about linear spaces of chains and their linear operators is given in Section 2. Section 3 introduces the concepts of -nearness of points and -congruence of cells in order to define the quotient topology. In Section 4.1 we introduce the block-diagonal sparse matrices which set the scene for the Cell Congruence Enabling algorithm, whose Julia code is discussed in Section 4. In Section 5 both an elegant implementation using native Julia syntax for sparse matrix and vector algebra is given, and its translation to fast GraphBLAS primitives [graphblas:20] is provided. The concluding section supplies some hints about possible uses of this approach.

2 Chain spaces and chain complex

All computer applications including geometric content may require some computation of topology, i.e., of the relations of incidence or adjacency between cells (vertices, edges, faces, volumes), i.e, between 0-, 1-,2-, and 3-cells of a cellular decomposition of either the input objects or their boundaries. Cells may be represented as basis elements in a graded vector space of chains, closed with respect to addition of cells with the same dimension and w.r.t. the product times a scalar element in a field [ieee-tase, Arnold:2018].

Given a finite collection S of geometric objects in , the arrangement is the decomposition of into connected open cells of dimensions induced by . We remark that the topology of a space partition is fully described by a chain complex, i.e. by a sequence of chain spaces of different dimension (), and by linear operators of boundary and coboundary between chain spaces. In particular, the union of bases of chain spaces provide a minimal set of generators of the induced topology. Once chosen an ordering of the cells, providing independent chain generators, such linear operators are described by their matrices, holding the scalar values which supply the coordinates with respect to the bases, that are normally either in (non-oriented chains) or in (oriented chains).

An important problem in computational geometry is to asses the arrangement [Halperin:2017, 2019arXiv191108130P] of Euclidean 3-space, i.e., the partition of produced by a collection of cellular 3-complexes. The simplest computational topology solution is provided by the computation of the chain complex

of the space partition generated by the input collection of geometric objects.

The chain of linear boundary operators is sufficient to characterize the linear chain spaces, since they contain the domain bases by columns, or better, they contain the coordinate vectors representing them as linear combinations of the target space bases. Furthermore, we have by duality, for every . The computation of the matrix, via the TGW (Topological Gift Wrapping) algorithm [2017arXiv170400142P], requires after the geometrical intersection of input 2-cells. In the present manuscript, we discuss how to compute the matrices , and , via elementary algebraic operations, implemented by using the standard GraphBLAS library for very large graphs.

3 Quotient topology

We introduce in this section a simple way to glue together a set of local geometric and topological information generated independently from each input 2-cell, by giving few definitions of nearness and congruence, which will allow to transform the union of local topologies into a single global “quotient topology”.

Let be a small positive value. Given a set of points, we can partition it into subsets such that where is a representative of , being the Euclidean distance. Such partition of a point set, characterized by a clustering of subsets of close elements, can be constructed using the inrange function of the NearestNeighbors.jl package, which implements the -d tree data structure [10.1145/361002.361007]. See the Section 4 for details.

3.1 Nearness

We say that two points are -near, and write , when their Euclidean distance is .

The -nearness is an equivalence relation, since it is reflexive, symmetric, and transitive. In particular, it is transitive since any pair of points are -near, because both have a distance less than from , and hence have a distance no more than from each other. More formally, if and , then , since the distance from of every point (e.g., ) in is less than .

Let be the quotient set of points w.r.t. the -nearness relation , for some fixed . The elements of can be chosen as the points representatives of the equivalence classes of . Even better, the representative of each class can be chosen as its centroid, and the average distance from of the other points of the class certainly decreases.

3.2 Topological congruence

Two geometric figures are congruent iff one can be transformed into the other by an isometry [Coxeter:1967], i.e. by an affine transformation with unit determinant.

We say that two -cells are -congruent, and write , when there exists a bijection between their 0-faces that pairwise maps vertices to -near vertices. The identification or quotient topology gives a method of getting a topology on from a topology on . The quotient topology is exactly the one that makes the resulting space ‘look like’ the original one, with the identified elements glued together.

A subset of open sets in the topological space over the cell complex is a basis for the topology of when all other open sets can be written as union of elements of . If we have an equivalence relation on a cell complex we get a natural projection map , by mapping each cell to its equivalence class. The identification or quotient topology on is defined as follows: a set is open if and only if is open in . In particular, the topology and the quotient topology are equivalent because any non-empty open set of contains a non-empty open set of and, conversely, every non-empty open set of contains non-empty open sets of .

It is easy to see that -congruence between elementary chains (aka cells) , denoted , is a graded equivalence relation, so that a chain complex may be represented by a much smaller , where , and where

4 Chain Complex Congruence

In this section we discuss the simple operations needed to transform the initial sparse block matrices and into the final operator matrices and concerning the space partition as a whole.

As we have seen, a chain -complex is completely specified by the sequence of its boundary or coboundary maps. Hence we intend to construct the coboundary maps induced by the congruence topology on the union set of local topological spaces generated by independent decompositions of input 2-cells, in the computational pipeline [2017arXiv170400142P] for the building of the space arrangement produced by a collection of cellular complexes.

Figure 2: Doubly nested structure of sparse block-matrices and .

4.1 Block-diagonal accumulators of coboundaries

We fix our attention here to and in particular to the computation of and . The matrix will be computed by Topological Gift Wrapping (TGW) algorithm, not described in this paper, in a following stage of the computational pipeline [2017arXiv170400142P], starting from .

In the algorithmic specification below, a dense array W and two sparse arrays Delta_0, Delta_1 respectively provide the input vertex coordinates and the sparse block-matrices and .

After independent computation, possibly in parallel, of local chain complexes (see [2017arXiv170400142P]), both accumulator matrices and have a sparse block-diagonal structure, made by two nested levels of (sparse) diagonal blocks. Each outer block concerns one of the input geometric objects, and inner blocks, , store the matrices of each decomposed 2-cell. We call , the exterior blocks (light gray), and , the interior blocks (dark gray), where is the number of 2-cells in -th input geometric object.

4.2 Nearness of vertices

First we need to discover the -nearness of vertices on the input matrix V of 3D coordinates, by querying the kdtree data structure [10.1145/361002.361007], and getting classes of -congruent vertices, represented by the array of arrays vclasses. The output matrix W holds by columns the coordinates of identified vertices in each -congruent class, that are mapped to their centroids.

1function vcongruence(V::Matrix; epsilon=1e-6)
2    vclasses, visited = [], []
3    kdtree  = NearestNeighbors.KDTree(V);
4    for vidx = 1 : size(V, 2)  if !(vidx in visited)
5        nearvs = NearestNeighbors.inrange(kdtree, V[:,vidx], epsilon)
6        push!(vclasses, nearvs)
7        append!(visited, nearvs)  end
8    end
9    W = hcat([sum(V[:,class], dims=2)/length(class) for class in vclasses]...)
10    return W, vclasses

4.3 Chain Complex Congruence Algorithm

In Section 4.1 we have discussed the block diagonal marshaling and of local coboundary matrices. With the function cellcongruence we replace each subset of columns of Delta sparse matrix corresponding to -near vertices with their vector sum. In this way we produce a new matrix from an array of new vectors. Finally, equal rows of this new matrix, discovered by a dictionary, are substituted by a single representative.

The same function cellcongruence is also applied to , by summing each subset of columns corresponding to each class of congruent edges, so generating a new sparse matrix from the resulting set of columns. Then, we reduce every subset of equal rows, if any, to a single row representative of congruent faces. A stepwise description of the Chain Complex Congruence (CCC) algorithm is given below. The function cellcongruence takes Delta of type SparseMatrixCSC and the inclasses given as array of arrays, and computes the set outclasses of congruent elementary -chains, represented as arrays of -chains.

  1. Lar.cop2lar transforms the sparse matrix into a cellarray (array of arrays of cell indices); then a vector newcell is defined to accomodate a map between old facet indices into the new ones of their quotient projection . Finally, the array cells accomodates the translated representations (arrays) of input (-1)-cells corresponding to columns of ;

  2. the empty cells, if any, are removed from okcells. Empty cells appear when they contain a number of distinct facets less than dim, i.e., for edges, and for faces;

  3. each -cell (elementary -chain) is represented as an array of indices of its facets (-1)-chain). The selection of single instances of output cells (from okcells, where they may be repeated) is performed using a DefaultOrderedDict for congruence classes, with key given by the cell representation (array of facets). The dictionary reading action classes[face] returns [] iff the key (face) is not contained in the dictionary. In this case the key is stored, and its storage of value starts with the first index [k]; otherwise, the current index is appended to the yet incomplete value.

  4. At the end, both the dictionary keys (column indices of congruent rows, i.e. the set of non-zero positions (for the representative row) in the output , and the dictionary values, i.e., the projected elements (class representatives) are given as output.

2function cellcongruence(Delta, inclasses; dim)
3  cellarray = Lar.cop2lar(Delta)
4  newcell = Vector(undef, size(Delta,2))
5  [ newcell[e] = k for (k, class) in enumerate(inclasses) for e in class ]
6  cells = [map(x -> newcell[x], face) for face in cellarray]
7  okcells = [cell for cell in cells if length(Set{cell))  dim]  # non-empty cells
8  classes = DefaultOrderedDict{Vector, Vector}([])
9  for (k,face) in enumerate(okcells)
10    classes[face] == [] ?  classes[face] = [k] : append!(classes[face], [k])
11  end
12  cells = collect(keys(classes))
13  outclasses = collect(values(classes))
14  return cells, outclasses

4.4 Top-level interface

Finally, the higher-level function chaincongruence maps the input data W, Delta_0, Delta_1, into a compact representation V, EV, FE of the chain complex , , and (see Appendix A.1). For full generality, also the 2-cell congruence was checked and taken into account. Congruent faces would in fact appear when some input 2-cells of the arrangement pipeline have inner intersection and lay on the same 3D plane. We would like to remark that decomposed 1-cells (i.e., input edges) and the representatives of their congruence classes (i.e., output edges) are associated one-to-one with the rows of and the columns of , respectively, so satisfying the topological constraints , that were checked positively in all our tests.

2function chaincongruence(W, Delta0, Delta_1)
3  V, vclasses = vcongruence(W)
4  EV, eclasses = cellcongruence(Delta_0, vclasses, dim=1)
5  FE, fclasses = cellcongruence(Delta_1, eclasses, dim=2)
6  return V, EV, FE
9V,EV,FE = chaincongruence(W,Delta_0,Delta_1)

5 Sparse matrix implementations

Two compact matrix implementations of Chain Complex Congruence algorithm are given in this section, starting from native coding in Julia, with its concise matrix syntax, then using the Julia porting of the package SuiteSparse:GraphBLAS

, enriched by a simple user-friendly interface from Julia’s multiple dispatch. The Julia code for the three implementations of the CCC algorithm is available at open-source repository

https://github.com/cvdlab/LocalCongruence.jl. The three implementations are denoted as AA (array of arrays), SP (Julia sparse array), and GB (GraphBLAS), respectively. The AA code was discussed in Section 4.

5.1 Julia’s SparseArrays.jl implementation

The vertexCongruence evaluates the Vertex Congruence for 3D-points. The function determines the points of matrix “V“ closer than “err“ to each other, and builds a new Vertex Set made of the representative of each point cluster. The method returns: the new Vertex Set; a map that, for every new vertex, identifies the subset of old vertices it is made of. The function cellCongruence evaluates the Cell Congruence for a -Cochain “cop“ of type sparse array, with equivalence classes “lo_cls“, and corresponding “lo_sign“, for -cells, both given as array of arrays of (old) lower-rank indices, to denote equivalence classes. The chainCongruence function performs the Geometry “G“ congruence and reshapes the Topology “T“.

5.2 Mapping to SuiteSparse:GraphBLAS.jl

6 Analysis of results

We have introduced here three versions of our CCC algorithm. A comparison with similar or different methods is clearly not possible, by the novelty of our approach, but a mutual comparison is interesting. The first algorithm, in Section 4, introduced the sparse block-matrix reduction method by using arrays of arrays, but with the drawback of losing the ordering information (and the sparse matrix output) of the cells, which were available in the input. The second version is given in Section 5.1 by directly using the Julia’s native sparse matrices. The third implementation, in Section 5.2, makes use of the GraphBLAS primitives within a tiny Julia’s wrapping.

Below you may see, using mean times, and assuming AA = 1x (Native Julia’s array of array — last column), that we have: native Julia (first column) SparseArrays = 16.6x; Julia wrapping (second column) of SuiteSparse:GraphBLAS = 6.12x.

1julia> @benchmark chainCongruence(W,T)
3  memory estimate:  19.47 MiB
4  allocs estimate:  782635
5  --------------
6  minimum time:     1.006 s (0.38% GC)
7  median time:      1.008 s (0.00% GC)
8  mean time:        1.008 s (0.16% GC)
9  maximum time:     1.010 s (0.00% GC)
10  --------------
11  samples:          5
12  evals/sample:     1
1julia> @benchmark chainCongruenceGB(W,T_GB)
3  memory estimate:  30.07 MiB
4  allocs estimate:  1077403
5  --------------
6  minimum time:     363.749 ms (0.00% GC)
7  median time:      371.598 ms (1.06% GC)
8  mean time:        370.236 ms (0.80% GC)
9  maximum time:     378.384 ms (1.20% GC)
10  --------------
11  samples:          14
12  evals/sample:     1
1julia> @benchmark chainCongruenceAA(G,T)
3  memory estimate:  9.33 MiB
4  allocs estimate:  154347
5  --------------
6  minimum time:     55.680 ms (0.00% GC)
7  median time:      59.759 ms (0.00% GC)
8  mean time:        60.497 ms (1.66% GC)
9  maximum time:     71.103 ms (7.29% GC)
10  --------------
11  samples:          83
12  evals/sample:     1

All implementations are quite naive. No optimizations were performed. We believe that major benefits may be ported by optimization to the two versions that use sparse matrices. A great caveat for the array of arrays version is that it looses track of cell signs, that must be recovered later with further computations, so losing all time benefits. Anyway, it let us suppose that the GraphBLAS approach can be forther optimized.

The two implementations with sparse matrices allow for maintaining the 2-cell orientation within the global chain complex. We have seen that the GraphBLAS implementation has two important benefits: (1) with parity of storage occupation, it allows to compute the largest chain complexes; (2) it also allows for block decomposed matrix computations, so opening the way to hierarchical computation of huge topological operators. Finally, we remark that the CCC algorithm is multidimensional, and its implementation may be extended, as is, to manage higher dimensional complexes, just by adding more cellcongruence call instances to chaincongruence function on page 4.4.

7 Conclusion

In this paper we have discussed an efficient algebraic algorithm to create the and sparse coboundary matrices, encoding the topological congruences between a set of chain complexes. In the algorithmic pipeline to construct the arrangement of Euclidean 3-space [2019arXiv191108130P], local chain complexes are generated independently from single fragmented input 2-cells, starting from a collection of cellular 2- or 3-complexes in 3D. The correctness of results, that might depend on numeric approximations of floating-point arithmetics, is checked by testing the matrix constraint . Therefore, we have given one possible topological solution to the robustness problem in geometric computations, in the line discussed by Christoph Hoffmann in [Hoffmann:2001], using epsilon geometry [10.1145/73833.73857]. The chain complex congruence (CCC) enabling algorithm introduced here was inplemented in Julia using the package SuiteSparseGraphBLAS.jl, and it is a key component of a computational pipeline to produce solid models of complex geometric scenes, using robust Boolean algebra methods for next-generation image understanding.


Appendix A Appendix

a.1 A simple example

For the sake of reproducibility, as well as of reader convenience and understanding, a very simple example of the congruence reduction for a 3D cube is given here, by using the initial implementation of Sections 4.2 and 4.3. The open-source realization with Julia sparse matrices and/or with GraphBLAS may by downloaded from GitHub at https://github.com/cvdlab/LocalCongruence.jl.

The cube was generated with random size and random attitude produced by a random 3D rotation, in order to start from close but unequal coordinates for each single instance of face vertices. All faces were generated independently, in order to get columns (vertex instances) in the input matrix W. The Julia’s matrix input follows (as matrix, in order to show the full Float64 digital resolution).

2julia> W = convert(Matrix,[0.5310492999999998 0.8659989999999999 0.14191280000000003; 1.0146684 0.6827212999999999 0.2169682; 0.3477716 0.5268921 0.4947971000000001; 0.8313907882395298 0.3436144447063971 0.5698524407571428; 0.6061046999999998 1.2188832999999994 0.5200012; 1.0897237999999998 1.0356056999999999 0.5950565999999999; 0.42282699999999984 0.8797763999999998 0.8728855; 0.9064461979373597 0.6964987903021808 0.9479408896095312; 0.5310493 0.8659989999999999 0.14191279999999987; 1.0146684000000001 0.6827213 0.21696819999999994; 0.6061047 1.2188833 0.5200011999999999; 1.0897237772434623 1.035605657895053 0.5950566438156151; 0.3477716 0.5268921 0.4947971; 0.8313908 0.3436145 0.5698525000000001; 0.422827 0.8797764000000001 0.8728855; 0.9064462 0.6964988 0.9479409; 0.5310493 0.8659989999999999 0.14191280000000006; 0.34777160000000007 0.5268920999999999 0.4947971000000001; 0.6061047 1.2188833 0.5200012000000002; 0.4228270000000002 0.8797764 0.8728855000000001; 1.0146684 0.6827213 0.21696819999999994; 0.8313908 0.3436145 0.5698525000000001; 1.0897238 1.0356057 0.5950565999999999; 0.9064461675456482 0.6964988122992563 0.9479408949632379]’);

The two corresponding sparse block-diagonal matrices Delta_0 and Delta_1 are given below, as array triples (I,J,X) of row and column indices, and non-zero values.

2julia> Delta_0 = SparseArrays.sparse([1, 3, 1, 4, 2, 3, 2, 4, 5, 7, 5, 8, 6, 7, 6, 8, 9, 11, 9, 12, 10, 11, 10, 12, 13, 15, 13, 16, 14, 15, 14, 16, 17, 19, 17, 20, 18, 19, 18, 20, 21, 23, 21, 24, 22, 23, 22, 24], [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24], Int8[-1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1, 1]);
2julia> Delta_1 = SparseArrays.sparse([1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], Int8[1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1]);

The execution command from Julia terminal is given below, followed by output showing, with V output by columns, and both EV (edges by vertex indices) and FE (faces by edge indices) as array of arrays:

2julia> V,EV,FE = chaincongruence(W,Delta_0,Delta_1);
2julia> @show V;  # centroids of the 8  classes of W points
3V = [0.531049 1.01467  0.347772 0.831391 0.606105 1.08972  0.422827 0.906446;
4     0.865999 0.682721 0.526892 0.343614 1.21888  1.03561  0.879776 0.696499;
5     0.141913 0.216968 0.494797 0.569852 0.520001 0.595057 0.872886 0.947941]
2julia> @show EV;  # edges-by-vertices
3EV = Array{T,1} where T[[1, 2], [1, 3], [1, 5], [2, 4], [2, 6], [3, 4], [3, 7], [4, 8], [5, 6], [5, 7], [6, 8], [7, 8]]
2julia> @show FE;  # faces-by-edges
3FE = Array{T,1} where T[[1, 2, 3, 4], [1, 5, 9, 10], [2, 6, 11, 12], [3, 7, 9, 11], [4, 8, 10, 12], [5, 6, 7, 8]]
Figure 3: The generated chain complex, with basis 0-chains (roman), 1-chains (red) and 2-chains (bold).