G+Smo  24.08.0
Geometry + Simulation Modules
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
thbSplineBasis_example.cpp

This tutorial provides a basic motivation for truncated hierarchical B-splines (THB-splines) and a brief introduction to some basic features of gsTHBSplineBasis.
See also basis_example.cpp and bSplineBasis_example.cpp.

More details on the hierarchical splines can be found in adaptRefinementThb_example.cpp as well as in the Hierarchical splines module description.

Introduction

Two simple examples are provided for illustrating the construction of a THB-spline basis. The formal definition is given only for the sake of completeness (see Formal definition), see the References for further details.

1D-example

For illustration, consider a 1D-example.
We start with two sets of B-Spline basis functions of degree 2:

Level 0, the "coarse" ones, with knot vector \( s^0 = (0,\ 0,\ 0,\ 1/8,\ 2/8,\ ... 7/8,\ 1,\ 1,\ 1)\)

Level 1, the "fine" ones, with knot vector \( s^1 = (0,\ 0,\ 0,\ 1/16,\ 2/16,\ ... 15/16,\ 1,\ 1,\ 1)\)

These two bases are illustrated here (shown in different colours and thickness only for better visibility):

tutTHB_1d_B0.png
B-splines of level 0
tutTHB_1d_B1.png
B-splines of level 1

The parameter domain is referred to as \( \Omega^0 \). The green area in the pictures indicates \( \Omega^1 \subset \Omega^0 \), i.e., the area where we want finer basis functions.

A straightforward way of doing so: Take all fine functions \( \beta^1_j \) where \( \mathrm{supp} \beta^1_j \subset \Omega^1 \),

tutTHB_1d_B1act.png
Active B-splines of level 1

...and only those coarse functions \( \beta^0_i \) where \( \mathrm{supp} \beta^0_i \subset \Omega^0 \land \mathrm{supp} \beta^0_i \not \subset \Omega^1 \)

tutTHB_1d_B0sel.png
Active B-splines of level 0

HB-splines

By taking the union of these two sets of functions, we obtain the hierarchical B-spline basis (HB-splines) of level 1:

tutTHB_1d_H1.png
HB-splines

With this basis, however, the partition-of-unity property is lost. Also, there can be large overlaps of the supports of the basis functions.

THB-splines

Instead of just using the coarse functions as they are, the coarse functions are truncated (see References at the bottom for details). This results in the THB-spline basis of level 1:

tutTHB_1d_T1.png
THB-splines

THB-splines again have the partition-of-unity property and support overlaps are reduced.

Note that HB-splines and THB-splines span the same function space.

2D-example

Similar to the 1D-case above, consider a coarse and a fine mesh, as displayed on the left, and right, respectively. The corresponding tensor-product B-spline bases of degree 2 are displayed in the picture below. The green area again indicates the area where we want fine basis funcitons.

tutTHB_2d_m0_m1.png
Meshes of levels 0 and 1, green are to be refined to level 1.


tutTHB_2d_B0_B1.png
Tensor-product B-spline bases of levels 0 and 1.


As in the 1D-case, we select
the coarse functions \( \beta^0_i \) where \( \mathrm{supp} \beta^0_i \subset \Omega^0 \land \mathrm{supp} \beta^0_i \not \subset \Omega^1 \), and we truncate them (displayed on the left)
and the fine functions \( \beta^1_j \) where \( \mathrm{supp} \beta^1_j \subset \Omega^1 \) (displayed on the right):

tutTHB_2d_B0_B1_acts.png
Active basis functions of levels 0 (already truncated) and of level 1.


The union of these functions is the THB-spline basis:

tutTHB_2d_Tm1_T1.png
Locally refined mesh and corresponding THB-spline basis.


Formal definition

Just for the sake of completeness, a brief formal definition is provided, without going into any details. See References at the bottom for details

Truncation operation

Let \( B^{\ell-1} \) denote the (tensor-product) B-spline basis of level \(\ell - 1 \). Let a function \( \tau \in \mathrm{span} B^{\ell-1} \) be represented in terms of \( B^{\ell} \) by \( \tau = \sum_{\beta^{\ell} \in B^{\ell} } c_\beta \beta^{\ell} \). The truncation of \( \tau \) is given by \( \mathrm{trunc}^{\ell} \tau = \sum_{\beta^{\ell} \in B^{\ell},\ \mathrm{supp} \beta^{\ell} \not \subseteq \Omega^{\ell} } c_\beta \beta^{\ell} \)

THB-spline basis

\( T^0 = B^0 \)
\( T^{\ell} = T_A^{\ell} \cup T_B^{\ell}\), where
\( \quad T_A^{\ell} = \{ \mathrm{trunc}^{\ell} \tau:\ \tau \in T^{\ell-1} \land \mathrm{supp} \tau \not \subseteq \Omega^{\ell} \} \)
\( \quad T_B^{\ell} = \{ \beta^\ell \in B^{\ell}:\ \mathrm{supp} \beta^\ell \subseteq \Omega^{\ell} \} \)
\( T = T^N \)

Constructing a THB-spline-basis

Remark: gsTHBSplineBasis and gsHBSplineBasis are derived from gsHTensorBasis

From tensor-basis

Constructor using a gsTensorBSplineBasis as argument.

// Set up and construct a knot vector...
real_t a = 0; // starting knot
real_t b = 1; // ending knot
unsigned interior = 3; // number of interior knots
int degree = 2;
unsigned multEnd = degree + 1; // multiplicity at the two end knots
gsKnotVector<> kv(a, b, interior, multEnd);
// ...a 2D-tensor-B-spline basis with this knot vector...
gsTensorBSplineBasis<2,real_t> tens( kv, kv );
// ...and a 2D-THB-spline basis out of the tensor-B-spline basis.
gsTHBSplineBasis<2,real_t> thb( tens );

From xml

Example for a THB-spline basis of degree 2:

<?xml version="1.0" encoding="UTF-8"?>
<xml>
 <Basis type="THBSplineBasis2" levels="6">
  <Basis type="TensorBSplineBasis2">
   <Basis type="BSplineBasis" index="0">
    <KnotVector degree="2">0 0 0 1 1 1 </KnotVector>
   </Basis>
   <Basis type="BSplineBasis" index="1">
    <KnotVector degree="2">0 0 0 1 1 1 </KnotVector>
   </Basis>
  </Basis>
 </Basis>
</xml>

The tensor-product B-spline basis in the "THBSplineBasis2" defines the basis of level 0.

Refinement

tutTHB_2d_m0_m1.png
Meshes of levels 0 and 1, green are to be refined to level 1.

using refineElements( std::vector )

See gsHTensorBasis::refineElements(). The argument is a std::vector<unsigned> containing a sequence of n*(1+2*d) unsigned. Each (1+2*d)-tuple contains the following information:

  • the the level that you want to refine to
  • the lower corner in unique knot span indices of the new level
  • the upper corner in unique knot span indices of the new level

The green are marked in the 2D-example above has lower corner (2,0) and upper corner (8,4) in unique knot indices at level 1. We can refine this area as follows:

std::vector<index_t> box;
box.push_back( 1 );
box.push_back( 2 );
box.push_back( 0 );
box.push_back( 8 );
box.push_back( 4 );
thb.refineElements(box);

This method is the recommended one, since the corners of the refinement areas are specified by unsigned integers, which is numerically stable. Also, since the level is specified explicitly, double calls by mistake will not result in double refinement.

by the "<box>" tag in the xml

One can also specify different levels of refinement already in the xml-file, using the "box"-tag:

<?xml version="1.0" encoding="UTF-8"?>
<xml>
 <Basis type="THBSplineBasis2" levels="6">   
  <Basis type="TensorBSplineBasis2">         
   <Basis type="BSplineBasis" index="0">
    <KnotVector degree="2">0 0 0 1 1 1 </KnotVector>
   </Basis>
   <Basis type="BSplineBasis" index="1">
    <KnotVector degree="2">0 0 0 1 1 1 </KnotVector>
   </Basis>
  </Basis>
 <box level="1">2 0 8 4</box>
 </Basis>
</xml>

using refine( gsMatrix )

See gsHTensorBasis::refine(). The argument is a gsMatrix of size d x (2*n). Two columns represent the coordinates of the lower and the upper corner, respectively, of the area to be refined. In our example, the area to be refined can be written as

gsMatrix<real_t> boxM(2,2);
boxM(0,0) = 0.25;
boxM(1,0) = 0.0;

boxM(0,1) = 1.0;
boxM(1,1) = 0.5;

thb.refine( boxM );

This method refines all elements overlapping the specified box.

Warning!
This method does not specify which level should be refined to. If parts overlap, areas might get refined twice.
This refinement function uses numerical values, which could cause problems, especially when the coordinates coincide with knots.

Basic operations

"standard" operations

Just like with the other bases. For example, displaying information via std::cout,

gsInfo << "this basis is:\n" << thb << std::endl;

will return the following:

This basis is:
Truncated basis of dimension 2,
levels=2, size=54, tree_nodes=5).
Domain: [0 0]..[1 1].
Size per level: 30 24 

The last line refers to the number of basis functions on the respective levels.

Other functions such as gsHTensorBasis::active_into(),
gsHTensorBasis::eval_into(),
gsHTensorBasis::deriv_into(), and also
gsHTensorBasis::uniformRefine() work as usual.

Example:

gsMatrix<real_t> u(2,3);
u(0,0) = 0.95;
u(1,0) = 0.05;
u(0,1) = 0.95;
u(1,1) = 0.3;
u(0,2) = 0.6;
u(1,2) = 0.9;
gsMatrix<index_t> resActives;
gsMatrix<real_t> resEvals;
thb.active_into( u, resActives);
thb.eval_into( u, resEvals);
gsInfo << "active functions: \n" << resActives << std::endl;
gsInfo << "their values: \n" << resEvals << std::endl;
tutTHB_2d_Tm1_evalPts.png
evaluation points


active functions: 
 9  9 14
10 10 15
11 11 16
33 15 20
34 16 21
35 17 22
39 45 26
40 46 27
41 47 28
45 51  0
46 52  0
47 53  0
their values:     
     0 0.0012 0.0144
     0 0.0204 0.0592
     0 0.0384 0.0064
0.0288 0.0004 0.1008
0.2016 0.0068 0.4144
0.1296 0.0128 0.0448
0.0448 0.0144 0.0648
0.3136 0.1008 0.2664
0.2016 0.0648 0.0288
0.0064 0.0592      0
0.0448 0.4144      0
0.0288 0.2664      0

Note:

  • The basis functions are numbered firstly by level (starting with level 0), and then by the tensor-index of the respective level (see Indexing / numbering of basis functions).

  • gsHTensorBasis::active_into() does not account for functions which are truncated to zero (i.e., it will return the same result for gsHBSplineBasis and gsTHBSplineBasis). Functions which are truncated to zero will be evaluated as (exactly) zero.

  • The numbers of active functions at the two evaluation points may different. Any shorter columns will be filled with (exact) zeros.

gsHTensorBasis - specific

Some additional functions are specific to gsHTensorBasis.

Indexing / numbering of basis functions

The basis functions are numbered firstly by level (starting with level 0), and then by the tensor-index of the respective level.
The index in the numbering of the whole THB-spline basis (in our example, the numbering from 0 to 53), is referred to as global index or hierarchical index.
The index that a certain function has within the tensor-product B-spline basis of the respective level is referred to as flat tensor index ("flat" in the sense that no d-tupels are used, but the tensor-product basis functions already have a "single-digit" index).

To get the flat index and the level of a certain basis function, use gsHTensorBasis::flatTensorIndexOf() and gsHTensorBasis::levelOf().

std::vector<unsigned> tmpFlatIndices;
std::vector<int> tmpLevels;
gsInfo << "transform indices\n";
gsInfo << "global/hier.index -> flat tensor index (on level)" << std::endl;
for( unsigned i = 27; i <= 35; i++)
{
// print computed indices/levels
gsInfo << i;
gsInfo << " -> ";
gsInfo << thb.flatTensorIndexOf(i);
gsInfo << " ( " << thb.levelOf(i) << " )" << std::endl;
// store indices/levels for reverse transformation later
tmpFlatIndices.push_back( thb.flatTensorIndexOf(i) );
tmpLevels.push_back( thb.levelOf(i) );
}

will, in our example, print

transform indices
global/hier.index  ->  flat tensor index (on level)
27  ->  33  ( 0 )
28  ->  34  ( 0 )
29  ->  35  ( 0 )
30  ->  4  ( 1 )
31  ->  5  ( 1 )
32  ->  6  ( 1 )
33  ->  7  ( 1 )
34  ->  8  ( 1 )
35  ->  9  ( 1 )

To compute the global/hierarchical index from a given flat index and a given level, use gsHTensorBasis::flatTensorIndexToHierachicalIndex().

gsInfo << std::endl;
gsInfo << "reverse index transformation\n";
gsInfo << "flat tensor index (on level) -> global/hier.index" << std::endl;
for( unsigned i = 0; i < tmpLevels.size(); i++ )
{
// print global/hierarchical indices
gsInfo << tmpFlatIndices[i] << " ( " << tmpLevels[i] << " )";
gsInfo << " -> ";
gsInfo << thb.flatTensorIndexToHierachicalIndex( tmpFlatIndices[i], tmpLevels[i] ) << std::endl;
}

will, in our example, print

reverse index transformation
flat tensor index (on level)  ->  global/hier.index
33  ( 0 )  ->  27
34  ( 0 )  ->  28
35  ( 0 )  ->  29
4  ( 1 )  ->  30
5  ( 1 )  ->  31
6  ( 1 )  ->  32
7  ( 1 )  ->  33
8  ( 1 )  ->  34
9  ( 1 )  ->  35

Get level and knot span

gsHTensorBasis::getLevelUniqueSpanAtPoints() will tell you the levels of certain points, as well as the knot span containing these points:

gsVector<index_t> resLevels;
gsMatrix<index_t> resLowerCorner;
thb.getLevelUniqueSpanAtPoints(u, resLevels, resLowerCorner);
gsInfo << "levels: " << std::endl << resLevels.transpose() << std::endl;
gsInfo << "lower corners: " << std::endl << resLowerCorner << std::endl;

In our example, this will return

levels: 
1 1 0
lower corners: 
7 7 2
0 2 3

These indices are given on the respective level.

Show tree-structure

The gsHTensorBasis are based on a gsHDomain. The tree-structure (see references at the bottom) can be accessed by the member function gsHTensorBasis::tree(). For example, this is how to take a look at the underlying tree-structure:

gsMatrix<index_t> resUpperCorner;
thb.tree().getBoxes( resLowerCorner, resUpperCorner, resLevels);
gsInfo << "levels: " << std::endl << resLevels << std::endl;
gsInfo << "lower corners: " << std::endl << resLowerCorner << std::endl;
gsInfo << "upper corners: " << std::endl << resUpperCorner << std::endl;

In our example, this will return

levels:        
0
1
0
lower corners: 
2 4
2 0
0 0
upper corners: 
8 8
8 4
2 8

Note that these indices are given with respect to level gsHDomain::m_maxInsLevel.

Remarks regarding refinement

Size of refined area

Keep in mind that fine functions are only added, if the refined area contains the support of at least one fine basis function! If only a single cell/element is refined, no fine functions are added (for \( p > 1 \))! If necessary, extend the refinement area by a ring of cells around the refinement area.

Additional refinement during refinement

Take the hierarchical mesh from above, where \( \Omega^1 \) is now coloured light green. Refining the dark green area to level 2 will NOT result in the mesh shown in the middle. Instead, in order to avoid L-shaped elements, and due to some requirements by the underlying tree, the mesh shown on the right will be generated (with additional refinement in the orange areas).

tutTHB_2d_Tm2.png
Level 1: light green. Level 2: dark green. Additional refinement: orange.

The hierarchical mesh and the corresponding THB-Spline basis:

tutTHB_2d_Tm2_T2.png
Hierarchical mesh and THB-splines on 3 levels.

Applying this refinement to the basis and printing the basis...

box.clear();
box.push_back( 2 );
box.push_back( 2 );
box.push_back( 4 );
box.push_back( 6 );
box.push_back( 10 );
thb.refineElements(box);
gsInfo << "after 2nd refinement, this basis is:\n" << thb << std::endl;

...will return

after 2nd refinement, this basis is:
Truncated basis of dimension 2,
levels=3, size=70, tree_nodes=25).
Domain: [0 0]..[1 1].
Size per level: 30 32 8 

This should be kept in mind when manually creating THB-spline geometries.

Preservation of coefficients

Assume you have given a certain spline geometry (curve, surface, volume,...) represented by tensor-product splines of level 0 with coefficients \( c^0_i \):

\[ G(\xi) = \sum_{ \beta^0_i \in B^0} c^0_i \beta^0_i(\xi) \]

The same geometry can be represented on level 1 with different coefficients \( c^1_j \):

\[ G(\xi) = \sum_{ \beta^1_j \in B^1} c^1_j \beta^1_j(\xi) \]

If an area \( \Omega^1 \subset \Omega^0 \) is refined to level 1, we obtain the THB-spline basis consisting of

  • truncated coarse functions \( \mathrm{trunc}^1 \beta^0_i(\xi), \mathrm{supp} \beta^0_i \not \subseteq \Omega^1 \)
  • and fine functions \( \beta^1_j, \ \mathrm{supp} \beta^1_j \subseteq \Omega^1 \)

The geometry \( G \) can be obtained exactly by taking the same coefficients \( c^0_j \) and \( c^1_j \) that were used on the respective levels:

\[ G(\xi) = \sum_{ \beta^0_i \in B^0,\ \mathrm{supp} \beta^0_i \not \subseteq \Omega^1} c^0_i\ \mathrm{trunc}^1 \beta^0_i(\xi) + \sum_{ \beta^1_j \in B^1,\ \mathrm{supp} \beta^1_j \subseteq \Omega^1} c^1_j \beta^1_j(\xi) \]

References

C. Giannelli, B. Jüttler, and H. Speleers. THB-splines: The truncated basis for hierarchical splines. Comput. Aided Geom. Design, 29(7):485-498, 2012. [Link] [Technical Report]

C. Giannelli, B. Jüttler, and H. Speleers. Strongly stable bases for adaptively refined multilevel spline spaces. Adv. Comp. Math., 40(2):459-490, 2014. [Link] [G+S Report No. 3]

C. Giannelli, B. Jüttler, S.K. Kleiss, A. Mantzaflaris, B. Simeon, J. Speh: THB-splines: An Effective Mathematical Technology for Adaptive Refinement in Geometric Design and Isogeometric Analysis. NFN Technical Report No. 30. [Link] [G+S Report No. 30]

G. Kiss, C. Giannelli, B. Jüttler: Algorithms and Data structures for truncated hierarchical B-splines. Mathematical Methods for Curves and Surfaces. Ed. by M. Floater et al. Vol. 8177. Lecture Notes in Computer Science. Springer, 2014, pp 304 - 323. [Link] [Technical Report]

Annotated source file

Here is the full file examples/thbSplineBasis_example.cpp. Clicking on a function or class name will lead you to its reference documentation.

// Look also in basis_example and bSplineBasis_example.
#include <string>
#include <gismo.h>
using namespace gismo;
int main(int argc, char *argv[])
{
bool plot = false;
gsCmdLine cmd("Tutorial on solving a Poisson problem.");
cmd.addSwitch("plot", "Create a ParaView visualization file with the solution", plot);
try { cmd.getValues(argc,argv); } catch (int rv) { return rv; }
// --------------- construction of a THB-spline basis ---------------
// Set up and construct a knot vector...
real_t a = 0; // starting knot
real_t b = 1; // ending knot
unsigned interior = 3; // number of interior knots
int degree = 2;
unsigned multEnd = degree + 1; // multiplicity at the two end knots
gsKnotVector<> kv(a, b, interior, multEnd);
// ...a 2D-tensor-B-spline basis with this knot vector...
// ...and a 2D-THB-spline basis out of the tensor-B-spline basis.
gsInfo << "basis before refinement:\n" << thb << std::endl;
// Export the initial basis to paraview files
gsWriteParaview(thb, "thb0_init" );
std::vector<index_t> box;
box.push_back( 1 );
box.push_back( 2 );
box.push_back( 0 );
box.push_back( 8 );
box.push_back( 4 );
thb.refineElements(box);
// Export the refined basis to paraview files
gsWriteParaview(thb, "thb_refined_first" );
gsInfo << "after refinement," << std::endl;
gsInfo << "this basis is:\n" << thb << std::endl;
// --------------- "standard" evaluations ---------------
u(0,0) = 0.95;
u(1,0) = 0.05;
u(0,1) = 0.95;
u(1,1) = 0.3;
u(0,2) = 0.6;
u(1,2) = 0.9;
gsMatrix<index_t> resActives;
gsMatrix<real_t> resEvals;
thb.active_into( u, resActives);
thb.eval_into( u, resEvals);
gsInfo << "active functions: \n" << resActives << std::endl;
gsInfo << "their values: \n" << resEvals << std::endl;
gsInfo << std::endl;
// --------------- index-computations ---------------
std::vector<unsigned> tmpFlatIndices;
std::vector<int> tmpLevels;
gsInfo << "transform indices\n";
gsInfo << "global/hier.index -> flat tensor index (on level)" << std::endl;
for( unsigned i = 27; i <= 35; i++)
{
// print computed indices/levels
gsInfo << i;
gsInfo << " -> ";
gsInfo << thb.flatTensorIndexOf(i);
gsInfo << " ( " << thb.levelOf(i) << " )" << std::endl;
// store indices/levels for reverse transformation later
tmpFlatIndices.push_back( thb.flatTensorIndexOf(i) );
tmpLevels.push_back( thb.levelOf(i) );
}
gsInfo << std::endl;
gsInfo << "reverse index transformation\n";
gsInfo << "flat tensor index (on level) -> global/hier.index" << std::endl;
for( unsigned i = 0; i < tmpLevels.size(); i++ )
{
// print global/hierarchical indices
gsInfo << tmpFlatIndices[i] << " ( " << tmpLevels[i] << " )";
gsInfo << " -> ";
gsInfo << thb.flatTensorIndexToHierachicalIndex( tmpFlatIndices[i], tmpLevels[i] ) << std::endl;
}
gsInfo << std::endl;
// --------------- some gsHTensorBasis-specific functions ---------------
gsVector<index_t> resLevels;
gsMatrix<index_t> resLowerCorner;
thb.getLevelUniqueSpanAtPoints(u, resLevels, resLowerCorner);
gsInfo << "levels: " << std::endl << resLevels.transpose() << std::endl;
gsInfo << "lower corners: " << std::endl << resLowerCorner << std::endl;
gsInfo << std::endl;
// print the underlying tree
gsMatrix<index_t> resUpperCorner;
thb.tree().getBoxes( resLowerCorner, resUpperCorner, resLevels);
gsInfo << "levels: " << std::endl << resLevels << std::endl;
gsInfo << "lower corners: " << std::endl << resLowerCorner << std::endl;
gsInfo << "upper corners: " << std::endl << resUpperCorner << std::endl;
// --------------- 2nd local refinement ---------------
gsInfo << std::endl << std::endl;
box.clear();
box.push_back( 2 );
box.push_back( 2 );
box.push_back( 4 );
box.push_back( 6 );
box.push_back( 10 );
thb.refineElements(box);
gsInfo << "after 2nd refinement, this basis is:\n" << thb << std::endl;
gsWriteParaview(thb, "thb_refined_second" );
boxSide side(1);
gsMatrix<index_t> result = thb.boundaryOffset(1,0);
gsInfo<<"Basis indices along side "<<side<<": "<<result.transpose()<<"\n";
for (index_t k=1; k<5; k++)
{
boxCorner corner(k);
gsInfo<<"Basis function index in corner "<<corner<<": "<<thb.functionAtCorner(corner)<<"\n";
}
// --------------- plot basis after 1 refinement ---------------
if( plot )
{
// Run paraview
gsFileManager::open("thb1_refined.pvd");
}
else
{
gsInfo << "Done. No output created, re-run with --plot to get a ParaView "
"file containing the solution.\n";
}
return EXIT_SUCCESS;
}