Alexandria 2.31.0
SDC-CH common library for the Euclid project
|
The GridContainer module provides a highly configurable, multi-dimensional grid, which provides extended functionality related with the information about its axes (in an efficient way). Note that this module is NOT a mathematical module. The provided GridContainer is closer to a collection structure than a mathematical grid and no mathematical functionality is provided.
This page contains multiple code examples. To improve readability, the following two lines are assumed for each example:
Note that these lines are used to make the example code more readable and they will introduce all the symbols of the GridContainer and std namespaces in the global namespace, so they must be sparingly used.
At the bottom of this page can be found the entire code of the examples as a single file.
The GridContainer data model consists of three concepts, the GridAxis class, which contains information of a grid axis, the GridCellManager, which handles the grid cell values and the GridContainer class, which combines everything together. The following sections describe these concepts and provide examples of how to use them.
Each axis of a GridContainer can be seen as a collection of (zero based) indexed knots (knot_0, knot_1, knot_2 etc). The index of each knot represents the coordinate of the grid cells and the value of the knot represents the world value of the axis. For example, an axis representing the wavelength might have the following knots:
Knot Index | Knot Value |
---|---|
0 | 3000 |
1 | 3500 |
2 | 4000 |
3 | 4500 |
4 | 5000 |
Using the above axis, to access the grid cells which have wavelength equal with 4000, the wavelength axis coordinate must be 2.
Constructing a GridAxis object which represents the above axis can be done using the following code:
In the code above one can make the following observations:
int
(GridAxis<int>
). To use knots of string
type, the definitions should be GridAxis<string>
. Note that user defined types are also supported.Retrieving the axis information from an GridAxis object is trivial. The knot values can be accessed either by using an iterator or directly, by using the brackets ([]
) operator, as demonstrated in the following example:
Output:
The GridContainer class does not implement in itself a data structure to hold the array of grid cell values. Instead, it delegates this task to a GridCellManager**, the type of which is provided as a template parameter to the GridContainer class (with signatures such as GridContainer<GridCellManager, ...>
, where the additional template parameters are related to the grid axes, see GridContainer class below).
Existing container classes can be used as the GridCellManager of a GridContainer, with the simplest example being the std::vector. For example, a GridContainer defined as GridContainer<vector<int>,...>
will use internally a vector to hold and manage the grid cell integer values.
The usage of custom GridCellManagers requires a better understanding of the GridContainer module in total, so it is postponed for later in this document (section Using a custom GridCellContainer).
As explained earlier, the GridContainer class is combining together the concepts of the GridCellManager (which keeps the GridContainer data) and the information of the corresponding axes (described as GridAxis objects).
Each GridContainer object has a fixed parameter space, meaning that, once created, its axes cannot be modified. For this reason, the GridAxis objects representing the GridContainer axes must be created beforehand. The following code demonstrates how to create a three dimensional grid:
The first template parameter of the GridContainer class defines the GridCellManager
type that the GridContainer will use. In the example above this type is vector<int>
, which shows that the GridContainer will keep integer values and it will use a vector as the GridCellManager.
The rest of the template parameters define the types of the axes world values. In our example the first axis has integer knots, the second double knots and the third strings. Note that the axes knots can be of any type, even of user defined types. The axes given as parameters to the constructor must be of the correct type, otherwise the compilation will fail. Note that in the example above, the vector for keeping the grid data is automatically created by the constructor, by using the GridCellManagerTraits::factory() method.
The second way of creating Grids is when one already has a GridContainer instance and want to create a second GridContainer with the same parameter space (axes). This is a common operation, so it is supported by the GridContainer API as demonstrated by the following code:
The code above creates a new grid of boolean cells, with the same axes like the parameter. Note that only the axes number and types need to match between the two grids. The new grid GridCellManager can be of any type.
The GridContainer class provides a range of methods for retrieving its information, as demonstrated by the following code:
Output:
Note that the axes information is retrieved by the GridContainer::getAxis() method, which returns a reference to an GridAxis object. The axis to get the information for is specified with an integer template parameter representing its (zero based) index.
The GridContainer cells can be directly accessed based on their coordinates, by using the parenthesis operator. This operator returns a reference to the cell value which can be used both for reading and writing:
Note that the parenthesis operator will not perform any bound checks to the given indices. If any of the indices is out of bounds will result to undefined behavior. For cases the indices are not guaranteed to be in bounds, the alternative method at() should be used (with the implied performance cost), which behaves the same way like the parenthesis operator, but it throws an Elements::Exception in the case any of the indices is out of bounds:
In the above example if the coord variable has a value less than the first axis size the code will print the cell value. If it is out of bounds, it will print the message of the exception.
Accessing the grid cells by using the parenthesis operator and the at() method is very useful for accessing a single cell of which the coordinates are known at the time of the call. This access though includes an overhead of the coordinates translation, so the use of the GridContainer iterator (described bellow) is recommended for any case a big number of cells is accessed.
The second way to access the GridContainer cells, both for reading and writing, is by using the GridContainer iterator. This method is the most efficient because it does not imply any overhead related with axes management. For example, the following code is almost as fast as accessing directly the vector keeping the values:
Output:
The order the cells are iterated is such so that the first axis varies the fastest and the last the slowest. Note that using the iterator for accessing the grid cells is the most efficient way, but it does not provide any information about the axes of each cell. This information can be retrieved by using the special methods of the GridContainer::iterator when needed (which implies some computing overhead):
Output:
The methods provided by the iterator for accessing the axes information are the GridContainer::iterator::axisIndex(), which returns the related coordinate, and the GridContainer::iterator::axisValue(), which returns the world value of the axis knot. Both methods receive the axis (zero based) index as an integer template parameter. Note that the overhead of the second method is higher than the one of the first, so it should be avoided in cases performance is an issue.
The GridContainer module provides very efficient iteration over slices of a GridContainer. This can be done with two ways:
The GridContainer::iterator::fixAxisByIndex() and GridContainer::iterator::fixAxisByValue() methods can be used for slicing, as demonstrated by the following code:
Output:
`
When a method for fixing an axis is called, the iterator moves to the next cell with the given axis coordinate (or stays where it is if the current cell has this coordinate). Increasing further the iterator will move it only through the cells of the slice.
The functions for fixing an iterators axes can be chained to fix more than one axes. For example, if in the previous code we had used:
we would get the following output:
The order in which the axes are fixed is not important, but keep in mind that if the current cell does not have the requested axis coordinate the iterator is shifted. For this reason it is best to fix the iterator axes right after retrieving the iterator with the GridContainer::begin() method. Fixing the same axis for a second time (moving to a different slice) is not allowed and an exception is thrown.
The second way to slice a grid is to use the methods gixAxisByValue() and fixAxisByIndex() of the GridContainer class itself. These methods will return a GridContainer object which represents the slice of the original grid. For example:
Output:
Note that the grid representing the slice has the same number of axes with the original grid, but the fixed axis has a single value:
Output:
The functions for slicing a grid can be chained to fix more than one axes. For example:
Output:
There are two important notes for when using grid slices. The first is that both the original grid and the slice use the same underlying data. This means that any modifications on the slice cells will be reflected to the original grid and vice versa. For example:
Output:
The second important note is that the const versions of the fixAxisByIndex() and fixAxisByValue() return an object of type const GridContainer. This is necessary because otherwise the returned slice could be used for modifying the original (constant) grid (because they share the same underlying data). This, with combination that the GridContainer does not provide a copy constructor, makes it impossible to store the result to a new GridContainer instance (the move constructor cannot be used because of the constness of the returned type). The solution to this problem is to use a const reference to store the result:
To be able to import and export GridContainer objects, the GridContainer module provides by default support for binary boost serialization. This support is enabled by including the GridContainer/serialize.h
file:
For the simple case that a vector is used as a GridCellManager
and the cell type, as well as all the axes types, are boost serializable, serialization of the grids can work out-of-the-box:
where GridType is the type of the variable grid.
To be more flexible, the GridContainer module provides GridContainer serialization to generic streams. To make the example more readable a string stream is used, but it can be easily replaced with file streams to support permanent storage, or by socket streams to transfer grids via the network.
The above example works without any input from the user because all the axes have types which are boost serializable. If an axis had a user defined type the compilation would fail, as boost would not know how to serialize its values. This is easily fixed by implementing the required methods to make the user defined type serializable. More information can be found in the boost serialization documentation, which can be found here.
Similarly with the axes types, the type of the cells of a grid must be boost serializable, otherwise it will not be possible to serialize the grid. Note that the GridCellManager itself does not need to be serializable, just the values it manages. By default serialization is disabled for all the GridCellManagers. To enable serialization for a specific GridCellManager, except of defining the related boost::serialization methods, the related specialization of the GridCellManagerTraits must have the enable_boost_serializable flag set to true.
By default the GridContainer module enables serialization only for vectors of types which are boost serializable.
A GridContainer can be unfolded into an Alexandria Table, which can, in turn, be serialized either into plain text or FITS files. However, keep in mind that this is operation has a single direction: there is no functionality provided to load a GridContainer from a Table. However, it can be useful for debugging purposes to serialize a GridContainer into a format easy to read from other tools or languages (i.e. astropy's Table)
To be able to export a Table, you need to include first GridContainer/GridContainerToTable.h
When you want to transform a Grid, you need to make sure the axes and cell types can be translated to the types supported by Row::cell_type, and given a name. This is done via the specialization of two set of traits: GridAxisToTable and GridCellToTable.
By default, Alexandria provides two specializations of GridAxisToTable:
Note that only one column can be generated for each axis, using the name assigned to the axis.
For GridCellToTable, a default specialization for any kind of scalar - integer and floating point - is provided. By default, the associated column name will be value
.
For composed cell values, as it would be SourceCatalog::Photometry, you will need to provide a custom specialization. For instance:
Note that addColumnDescriptions uses the first value on the grid as a model for every other cell. This code assumes that all instances look alike.
From the examples above can be seen that the API of the GridContainer module is quite verbose. This is because of the flexibility the API provides, which leads in lengthy template definitions. In real life applications though, there are many cases that a set of (different cell type) grids is required for a fixed parameter space. The following is a suggestion of how to simplify the use of the GridContainer API for these cases.
To avoid the error prone use of integers as axes indices for all the methods requiring them as a template parameter, an enumeration can be used, so more meaningful names will replace them:
Note that an anonymous plain enum has been used. This is because we want to be able to use the conversion of the enumeration name to integer directly, without having to perform casts (as is required by the enum class).
To avoid repetition of the lengthy GridContainer declarations a specialized alias can be used:
The above definition assumes that all the grids will use a vector as a GridCellManager
and allows only for further customization of the grid cells type. If different GridCellManagers
are to be used, the following (more generic) definition can be used:
The following code demonstrates how the code using the above specializations looks like. Note that the same examples like the previous sections are used, to make easier the comparison.
To achieve maximum flexibility, the GridContainer never use directly the GridCellManager** instance itself. Instead, it uses the GridCellManagerTraits class, which provides an interface to manipulate the grid cell values. In the case of a std::vector<int> GridCellManager, the GridContainer will make calls to a GridCellManagerTraits<std::vector<int>>.
The GridCellManagerTraits must implement the following interfaces:
GridCellManager
. This is the type "int" in our above vector example.GridCellManager
instance, which manages a given (fixed) number of cells.GridCellManager
.The default implementation of the GridCellManagerTraits will redirect all the calls to the GridCellManager. If the type used as GridCellManager provides all the required functions, it can be used directly.
The module also provides a specialization for all the std::vector types. This specialization behaves the same way like the default one, but it enables the boost serialization flag.
The following sections describe how to specialize the GridCellManagerTraits for types which are not providing the required API.
As an example of a type which can be used as a GridCellManager but it does not follow the GridCellManagerTraits API, imagine that we want to use as GridCellManager an old-style class which uses pointers to handle an area in the memory:
As can be seen above, the MemoryManager
class uses malloc
to allocate enough memory for the required number of objects, and frees it when it is destroyed. It provides direct access to the memory by returning a pointer to the beginning of it (note that the above implementation is incomplete and only for demonstration purposes). The MemoryManager
class is not compliant with the GridCellManagerTraits interface, so, if we want to use it as a GridCellManager, we need to provide a traits specialization.
The GridCellManagerTraits defines the behavior a GridCellManager type must provide, so it can be used by the GridContainer class. In other words, it provides the interface of the GridCellManager for the GridContainer to use. This is done so a type which provides a different interface can be used as a GridCellManager, by just defining a specialization of the GridCellManagerTraits. Note that the GridCellManagerTraits default behavior is to delegate all the operations to the type used as a GridCellManager. If the type is compliant with this interface, there is no need for defining the GridCellManagerTraits specialization.
The specialization of the GridCellManagerTraits for our MemoryManager
type can be done with the following code:
Note that it is not obligatory to implement all the above, but only what is actually being used. Furthermore, there is no requirement for the GridCellManager
to actually keep the data in the memory. An implementation can provide an iterator which will calculate the data on the fly. Such types of GridCellManagers are too application specific, so they are not described further here.