G+Smo  23.12.0
Geometry + Simulation Modules
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Tutorial 01: Hello G+Smo

In this tutorial you will get to know the structure of the library, some main tools which are available as well as how to write your first program.

Code repository

Obtaining and compiling

The source code can be obtained via Subversion or Git. See the section Downloads of the wiki for downloading information.

The configuration/compilation is based on the CMake build management system. A C++ compiler should be available in the system. See the section Compiling of the wiki for information on compilation.

The source folder

The source folder contains the following sub-folders:

  • src

    In this folder we have the source files of the library. Code is partitioned into modules, which correspond to folders inside src. Here is a list of these folders/modules:

    • gsCore

      Contains several general objects that are useful for the rest of the modules. Most importantly it contains abstract classes such as gsFunction and gsBasis which represent functions and sets of bases on a parametric domain. Continue here.

    • gsMatrix

      Contains classes for matrices, vectors, and sparse matrices. These objects are derived from the Eigen linear algebra library.

    • gsNurbs

      Contains classes for knot-vectors, univariate B-splines, tensor-product B-splines as well as NURBS and tensor-product NURBS classes.

    • gsHSplines

      Contains classes for hierarchical B-spline basis, as well as truncated hierarchical B-spline basis. These are based on a tree-structure which holds the domain information. It also contains adaptive fitting algorithms using hierarchical splines.

    • gsModeling

      Contains classes related to modeling with geometric objects. The main functionality is currently fitting and boundary-represented solids.

    • gsPde

      Contains classes which represent boundary value problems of partial differential equations. This includes the PDE definitions, boundary conditions and point loads.

    • gsAssembler

      Contains classes which allow us to generate (assemble) the matrices and right-hand side(s) of a boundary value problem by means of (Gauss) quadrature. Also, it contains classes which allow us to compute the norms or semi-norms of the difference of two functions, error estimators and related utilities.

    • gsSolver

      Contains iterative linear solvers which complement the ones from the Eigen library, such as GMRES. It also contains multigrid solvers and preconditioners.

    • gsIO

      Contains utilities related to data input and output. This includes helpers for adding command line arguments to executables, reading data from files and writing data to the disk. It implements the XML file format of G+Smo. Finally, there are functions which generate Paraview files for visualization.

    • gsTensor

      Contains classes and function to manipulate tensor-structured objects.

    • gsUtils

      Contains several utilities that are needed by other modules. This includes combinatorics utilities, point grid generators and iterators, utilities for measuring the CPU time, and a data structure for triangular meshes.

  • examples

    Contains source code for the examples, small programs and tutorials. Each of these files has extension .cpp and produces an executable upon compilation.

  • filedata

    Contains some example data files in the XML format the G+Smo can read and write.

  • optional

    Optional additional modules that can be compiled along G+Smo, typically extending functionality, sometimes also requiring external packages, which may be automatically downloaded. For instance, the Parasolid module requires the Parasolid library, and IpOpt module requires the IpOpt optimization library. The OpenNurbs module is used to read and write the Rhino3D 3DM file format, but it does not require any external package, since the source code of the OpenNurbs library is already in /external.

  • plugins

    The plugins are typically dynamic libraries which wrap G+Smo for linking with external software and for exporting functionality to third party software. A plugin is available for the Axel geometric modeller (INRIA).

  • cmake

    Contains Cmake configuration script files which are required for compilation.

  • doc

    Contains files related to Doxygen documentation pages.

The build (compilation) folder

The build folder has the following subfolders and files:

  • lib

    In this folder the G+Smo library is created. On MS Windows the dynamic library will be called gismo.dll, while on linux libgismo.so and on OSX libgismo.dylib. The static libraries are gismo_static.lib and libgismo_static.a, respectively. When typing "make -j2 gismo" in the root build folder the dynamic library is created. If you need a static library then "make -j2 gismo_static" will do the trick.

  • bin

    Inside this folder all the compiled executable files will be created upon compilation. In particular, each .cpp file in the examples folder corresponds to the file in this folder, with the same name. On MS Windows, the file will have extension .exe, while on Linux they will be marked as executable files. If this folder is empty, then type "make -j2 examples" in the root build folder to trigger the compilation.

    On a command line, you can compile (and execute) specific examples, for instance to execute the executable coming from examples/gsView.cpp it suffices to issue inside the build folder:

    $ make gsView
    [100%] Building CXX object stable/examples/CMakeFiles/gsView.dir/gsView.cpp.o
    [100%] Linking CXX executable ../../bin/gsView
    [100%] Built target gsView
    $ ./bin/gsView
    Hi, give me a file (eg: .xml) and I will try to draw it!
    Type ./bin/gsView -h, to get the list of command line options.
    $

    Note that on Linux you need to provide correctly the path (starting with dot-slash-bin), in order to execute a file. Similarly on Windows you should execute (eg. from a command line) the file gsView.exe

  • doc

    This folder contains files related to Doxygen documentation. In particular, if Doxygen is available on the system, and inside the root build folder we execute:

    $ make doc
    [100%] Generating API documentation with Doxygen
    [100%] Built target doc
    $ firefox doc/html/index.html

    then the resulting main Doxygen page will be created in doc/html/index.html and can be opened with a web-browser.

  • CMakeFiles

    This folder contains files related to the CMake configuration. Similarly several files (eg. with extension .cmake) which are found in the root folder are related to CMake. Most important of the latter files is the CMakeCache.txt file which keeps all the needed paths (eg. the path to the source folder) and all compilation flags and options.

  • src, examples, external, optional

    These folders are named as the corresponding folders in the source folder, and contain the compiled object files. All files in these folders are temporary, and their goal is to allow one to re-compile the library fast when there are only a few changes in the source files (while all the other object files are unchanged). Most of the time we do not need to care about or work in these folders.

Build types

There are a number of options which parametrize the compilation process. The default options should work most of the time. For a list of the available options have a look at this wiki page.

The most important setting is setting the build type. As common to the CMake build management system, there are three basic build types:

Build type Characteristics
Debug No optimizations, NDEBUG not defined
RelWithDebInfo Optimized code, but NDEBUG defined
Release The macro NDEBUG is defined, fully optimized

The default build type (if not specified with the CMAKE_BUILD_TYPE setting) is Release. Contrarily to the default setting of CMake, in G+Smo the macro NDEBUG is defined in this setting. Therefore in RelWithDebInfo mode you still get a fairly good checking level, with the cost that the performance is reduced.

The Debug mode is often too slow. However, as a last resort for chasing bugs this mode should be tried. Several times RelWithDebInfo inlines functions, and causes the debug information to point to the wrong line in the code.

The Release mode is the fully optimized, production code.

Getting started

A toy program using the library looks as follows.

# include <gismo.h>
using namespace gismo;
int main(int argc, char* argv[])
{
gsInfo << "Hello G+Smo.\n";
real_t a = 2.0; // a real number, ie. double
index_t b = 3; // an integer, ie. int
GISMO_ASSERT( a*b == 6, "This is an error, 2*3 should be 6.");
return 0;
}

All the objects of the library are found inside the gismo namespace.

Streaming text messages

There are pre-defined streams that should be preferred:

  • gsInfo This stream is an alias for std::cout and should be preferred over it.
  • gsWarn This stream prints the word "Warning" before your message, and should be used to warn of any important events.
  • gsDebug The messages are prefixed by GISMO_DEBUG and appear in Debug/RelWithDebInfo but not in release mode (controlled by the macro NDEBUG).
  • gsDebugVar(var) Streams the variable var, and the line and file of the code which emitted the message. This macro is used for easy printing and inspecting the contents of a variable.

Note that the end-of-line character "\n" should be preferred over std::endl .

Assertion macros

In Debug/RelWithDebInfo the assertions of the type are executed:

GISMO_ASSERT( condition, "Message if failed")

In Release Mode these commands are striped out of the code. An assertion which executes even in release mode is the following:

GISMO_ENSURE( condition, "Message if failed")

Also, the following emits an runtime error exception (without check condition):

GISMO_ERROR("You reached a line in the code which indicated a serious error")

Arithmetic types

Common arithmetic types are defined by macros in G+Smo, and should be preferred:

  • real_t This type defaults to double
  • index_t This is the number type used for all matrix and vector indices and sizes (eg. number of rows).
  • size_t This is the type used in the standard library, eg. for the size of an std::vector.

Note that index_t and size_t might be different types, therefore care should be taken when comparing their value.

Many template classes (for instance matrices) have a default first argument which is in most cases real_t.

Basic input and output

Command line arguments

A handy class called gsCmdLine for adding command line options to your programs. Adding options to a program makes it more friendly to the users, while allowing you to execute many different scenarios without having to change the source code and re-compile the source file.

Command-line arguments are useful to avoid re-compiling your code every time a small parameter is altered, as well as to make your program easily accessible by others. In G+Smo there is an easy way to add command-line arguments, which come automatically with a small help message and error checking, see commandLineArg_example.cpp.

In the file gsView.cpp for instance, we find the following code for parsing the command line:

gsCmdLine cmd("Hi, give me a file (eg: .xml) and I will try to draw it!");
cmd.addSwitch("geometry", "Try to find and plot a geometry contained in the file", get_geo);
cmd.addSwitch("mesh" , "Try to find and plot a mesh contained in the file", get_mesh);
cmd.addSwitch("basis" , "Try to find and plot a basis contained in the file", get_basis);
cmd.addInt ("s", "samples", "Number of samples to use for viewing", numSamples);
cmd.addSwitch("element" , "Plot the element mesh (when applicable)", plot_mesh);
cmd.addSwitch("controlNet", "Plot the control net (when applicable)", plot_net);
cmd.addSwitch("pid" , "Plot the ID of each patch and boudanries as color", plot_patchid);
cmd.addPlainString("filename", "File containing data to draw (.xml or third-party)", fn);
cmd.addString("o", "oname", "Filename to use for the ParaView output", pname);
try { cmd.getValues(argc,argv); } catch (int rv) { return rv; }

The last argument of each "add" command is, as expected, a boolean (for the addSwitch), or an integer (for the addInt). The gsCmdLine::getValues command parses all the user's input and updates the variables with new values. Therefore, after this command, the value of all the variables can be different. If an argument is not provided by the user, then the value of the variable does not change.

After compiling the program we can issue:

$ ./bin/gsView -h

and we obtain a list of the arguments together with the short explanation that we provided in the definition of the argument. Note that all arguments are checked automatically during parsing, eg. duplicate arguments and other subtle errors are detected.

Combined with reading the input data from files, this feature allows you eg. to write code which works for any input file which contains data that G+Smo can read, merely by adding a command line argument which accepts a string describing a filename.

The XML file format

The native format for data input, output and data exchange is XML. In G+Smo we define a number of tags that are used to read and write respective objects (classes). Typically, the data in an XML tree which represent, for instance, a B-spline, follow the logic of the classes in the code. As a first example the file square.xml is:

<?xml version="1.0" encoding="UTF-8"?>
<xml>
<Geometry type="TensorBSpline2" id="1">
<Basis type="TensorBSplineBasis2">
<Basis type="BSplineBasis" index="0">
<KnotVector degree="1">0.00000 0.00000 1.00000 1.00000</KnotVector>
</Basis>
<Basis type="BSplineBasis" index="1">
<KnotVector degree="1">0.00000 0.00000 1.00000 1.00000</KnotVector>
</Basis>
</Basis>
<coefs geoDim="2">0 0 1 0 0 1 1 1 </coefs>
</Geometry>
</xml>

The space between the numbers and line breaks do not matter. The plain text files can become big in size, when the data is a lot. For this reason, a compressed XML format is also supported, which can also be read/written directly.

All the tags in the XML tree of a G+Smo XML file must be contained inside the <xml > tag. Arbitrary data can be contained inside an XML file, as long as they are valid XML files. If you write (a small) XML file yourself, you can validate it for instance this page to make sure that there are no errors. In any case, invalid files will not be read properly by G+Smo and errors will be emitted. Each object in a G+Smo XML is also expected to have a unique ID attribute, which can be used for identifiing the specific data in the file.

The XML file format is extensible. As soon as objects/classes are created, a new tag for reading or writing the data can be added in the XML manager.

Reading files

To read a file in your program the recommended solution is to first define an input argument for your program which will be a string holding the name of the filename. For instance

std::string input("curves3d/bspline3d_curve_01.xml");
std::string output("");
gsCmdLine cmd("Tutorial Input Output");
cmd.addPlainString("filename", "G+Smo input geometry file.", input);
cmd.addString("o", "output", "Name of the output file", output);
try { cmd.getValues(argc,argv); } catch (int rv) { return rv; }

In the above code note the default value of the input string: The macro GISMO_DATA_DIR always points to the filedata folder inside the source folder. Using this macro allows you to make sure that your default argument will work on any system which will compile the library. Using an absolute path such as /home/myname/gismo/filedata/square.xml will directly make your program to fail when used in a different system. Therefore the GISMO_DATA_DIR macro is highly recommended.

The main class which allows you to read data from the file is the gsFileData object. In the following we read a polymorphic gsGeometry object from the file:

{
gsWarn << "The file cannot be found!\n";
return EXIT_FAILURE;
}
gsInfo << "Read file \"" << input << "\"\n";
gsFileData<> fileData(input);
gsGeometry<>::uPtr pGeom;
if (fileData.has< gsGeometry<> >())
{
pGeom = fileData.getFirst< gsGeometry<> >();
}
else
{
gsWarn << "Input file doesn't have a geometry.\n";
return EXIT_FAILURE;
}

When creating the gsFileData object, all the XML data in the file are loading into memory as an XML tree. This XML tree can contain many different objects. In the above, the gsFileData::has helper searches the tree for the existence of a gsGeometry tag. This tag corresponds to a polymorphic geometry object (which can be a polynomial curve, surface or volume or a rational NURBS patch, or a different type of geometry/patch). In the case that a gsGeometry object is found, a pointer is allocated and returned to the user. As the name of the get function suggests, the returned object is the first object which was encountered in the XML tree.

To see what is the gsGeometry data that have been obtained, we can stream it to gsInfo:

gsInfo << "The file contains: \n" << *pGeom << "\n";

You can have better control of which data is read from the file by reading using the ID of the tag. For instance, the following:

// Surface fitting
// Expected input is a file with matrices with:
// id 0: u,v -- parametric coordinates, size 2 x N
// id 1: x,y,z -- corresponding mapped values, size 3 x N
gsFileData<> fd_in(fn);
gsMatrix<> uv, xyz;
fd_in.getId<gsMatrix<> >(0, uv );
fd_in.getId<gsMatrix<> >(1, xyz);

reads two matrices from an XML tree, that correspond to parameters and values for a fitting problem. The uv matrix is fetched from the file as the matrix inside the file with id=0, and then the xyz matrix is fetched again by its id=1. this way there is no ambiguity which object is being read from the tree (as long as we use unique ids).

Another command which appears in the above example is the safe function. This macro takes a pointer to a matrix (which is the returned type of getId, and produces a smart pointer. The smart pointer is then transferred to uv/xyz matrices. This is an emulation of the move semantics of C++11, which we do not use yet, for the sake of compatibility.

Todo:
Update this paragraph

Writing files

Similarly to reading XML data, we are able to create XML tree data and save them as a file in the disk:

// writing a G+Smo .xml file
gsFileData<> fd;
fd << *pGeom;
// output is a string. The extention .xml is added automatically
fd.save(output);
gsInfo << "Wrote G+Smo file: " << output << ".xml \n";

the << operator translates the object into an XML tag. Then the save command creates a file with all the contents of the fd XML tree. Since we only pushed one object in the tree up to now, only one gsGeometry tag will be found in the resulting file.

Apart from the native XML format of the library, a number of third party formats can be imported or exported. One such useful format is Rhino's 3DM file format (note: the gsOpennurbs submodule must be enabled). Also, the Siemens' NX (Parasolid) file format is supported, if the Parasolid library is available in the system (note: GISMO_WITH_PSOLID compilation parameter). Other formats include the GeoPDE (Matlab/octave) text format, the GoTools format and OBJ, OFF and STL mesh files. The IGES file format is under development.

Matrices and linear algebra

The matrices in G+Smo are obtained from the Eigen linear algebra library.

All the functionality of Eigen::Matrix is available in the (inherited) class gsMatrix. The linearAlgebra_example.cpp contains starting examples for using matrices.

The sparse matrix class is gsSparseMatrix and is derived from Eigen::SparseMatrix class. Several sparse linear solvers are available, demonstrated in sparseSolvers_example.cpp. The main information about the matrix manipulations can be found in the Eigen documentation pages. We link to the most important chapters:

The quick reference pages give a summary of the functionality:

There is also a list with the correspondance to Matlab functions. However, note that some operations of Matlab are not mirrored (yet) with functionality in Eigen. For example, a reshape function does not exist. The Topic Aliasing is also important to learn how to avoid bugs and write more efficient code.

Note
You do not need to care about headers and inclusion of Eigen files. By default the Core, Dense and Sparse packages are included in in G+Smo.

All the information and functionality in the above pages apply to the gsMatrix, gsVector and gsSparseMatrix, gsSparseVector classes. For certain functionalities, which we need to extend or adapt in G+Smo, for example for the sparse liner solver classes, there are more objects defined which correspond to Eigen objects. In general, we try to avoid the direct use of the Eigen namespace inside the library, whenever possible.

The sources of the Eigen linear algebra library are included in the code repository of G+Smo, for your convenience.

Matrix operations of Eigen should be preferred over using for loops or manual functions. One reason for this is the clarity of the code, the reduction of lines of code and last but not least the fact that the provided operations are vectorized therefore they are more efficient than manual coding.

As a simple example, consider that you would like to multiply each row of a matrix by a number. Here are some equivalent ways to do this:

#include <gismo.h>
using namespace gismo;
int main(int, char**)
{
gsInfo.precision(3);
// Task: multiply the rows of M by the corresponding scalars in v
gsVector<> v(4);
gsMatrix<> M(4,6);
v.setRandom();
M.setRandom();
// Manually
gsMatrix<> R1(4,6);
for (index_t i = 0; i<4; ++i) // for all rows
for (index_t j = 0; j<6; ++j) // for all columns
R1(i,j) = M(i,j) * v[i];
// Using row-blocks
gsMatrix<> R2(4,6);
for (index_t i = 0; i<4; ++i) // for all rows
R2.row(i).noalias() = M.row(i) * v[i]; // matrix x scalar
// Using only one line
R3.noalias() = v.asDiagonal() * M; // matrix x matrix
gsInfo << "v is: \n" << v << "\n\n";
gsInfo << "M is: \n" << M << "\n\n";
gsInfo << "R1 is: \n" << R1 << "\n";
gsInfo << "R2 is: \n" << R2 << "\n";
gsInfo << "R3 is: \n" << R3 << "\n";
return 0;
}

Output:

v is: 
  0.68
-0.211
 0.566
 0.597

M is: 
  0.823  -0.444   -0.27   0.271  -0.967  -0.687
 -0.605   0.108  0.0268   0.435  -0.514  -0.198
  -0.33 -0.0452   0.904  -0.717  -0.726   -0.74
  0.536   0.258   0.832   0.214   0.608  -0.782

R1 is: 
    0.56   -0.302   -0.184    0.185   -0.658   -0.467
   0.128  -0.0228 -0.00566  -0.0918    0.109   0.0418
  -0.187  -0.0256    0.512   -0.406   -0.411   -0.419
    0.32    0.154    0.497    0.128    0.363   -0.467
R2 is: 
    0.56   -0.302   -0.184    0.185   -0.658   -0.467
   0.128  -0.0228 -0.00566  -0.0918    0.109   0.0418
  -0.187  -0.0256    0.512   -0.406   -0.411   -0.419
    0.32    0.154    0.497    0.128    0.363   -0.467
R3 is: 
    0.56   -0.302   -0.184    0.185   -0.658   -0.467
   0.128  -0.0228 -0.00566  -0.0918    0.109   0.0418
  -0.187  -0.0256    0.512   -0.406   -0.411   -0.419
    0.32    0.154    0.497    0.128    0.363   -0.467

From these 3 versions, the last one is preferred, since we know well from linear algebra that left multiplication by a diagonal matrix corresponds to multiplying the rows by the diagonal elements. Note that v.asDiagonal() does not make a copy of the vector v, but only returns an expression of the v as a diagonal matrix. Also note the noalias() function to avoid Aliasing, eg. an un-needed temporary copy of R3.

Functions and bases

One of the most basic objects in G+Smo are the ones representing functions. There are several implementations of functions, or sets of functions. They all derive by a common class, gsFunctionSet. The main descendants of gsFunctionSet are two:

gsFunction

This class represents a (abstract) functions of several variables, possibly vector-valued. The function is defined over an (arbitrary) square domain, referred to as its support. One characteristic of the function is that in principle it takes non-zero values throughout its support.

The main attributes of functions (and bases) are the dimension of the parameter domain and the target domain, as well as their support. The main functionality is the ability to evaluate the function and its derivatives at given points inside the support.

One instance of function is the gsFunctionExpr. This class can be defined by a string containing the mathematical formula of a function. It makes it very easy ad efficient to define and work with complicated mathematical function with minimal effort.

gsBasis

This class represent an (abstract) function basis, that is, a set of basis functions. The functions are usually local, that is, they take non-zero value only at a small portion of the support of the basis. See basis_example.cpp

gsGeometry

One more instance of a function (derived from gsFunction) is the gsGeometry. This class is essentially a function defined by a coefficient vector and a gsBasis. For example a NURBS patch will fall in this category.

Printing the geometry:

gsInfo << "The file contains: \n" << *pGeom << "\n";
// G+Smo geometries contains basis and coefficients
const gsBasis<>& basis = pGeom->basis();
gsInfo << "\nBasis: \n" << basis << "\n";
const gsMatrix<>& coefs = pGeom->coefs();
gsInfo << "\nCoefficients: \n" << coefs << "\n" << "\n";

Printing the main properties:

gsInfo << "Dimension of the parameter space: " << pGeom->parDim() << "\n"
<< "Dimension of the geometry: " << pGeom->geoDim() << "\n";
// support of the geometry, this is the same as gsBasis::support
// (dim x 2 matrix, the parametric domain)
gsMatrix<> support = pGeom->support();
gsInfo << "Support: \n"
<< support << "\n" << "\n";

Computing values and derivatives:

gsMatrix<> u = 0.3 * support.col(0) + 0.7 * support.col(1);
gsInfo << "u " << size(u) << ": \n" << u << "\n" << "\n";
// geoDim x 1 matrix
gsMatrix<> value = pGeom->eval(u);
// geoDim x parDim matrix (columns represent gradients)
gsMatrix<> der1 = pGeom->deriv(u);
// [geoDim * (parDim + parDim * (parDim - 1) / 2)] x 1 matrix
gsMatrix<> der2 = pGeom->deriv2(u);
gsInfo << "Value at u " << size(value) << ": \n"
<< value
<< "\n\nDerivative at u " << size(der1) << ": \n"
<< der1
<< "\n\nSecond derivative at u " << size(der2) << ": \n"
<< der2
<< "\n" << "\n";