Grid and axis

The Grid template class provides a generic binned Grid implementation in $N$ dimensions. Grid accepts a variadic list of Axis types, where the number of axes equals the number of desired dimensions of the Grid.


Axis accepts two template parameters:

template <AxisType type,
          AxisBoundaryType bdt = AxisBoundaryType::Open>
class Axis;


enum class AxisBoundaryType { Open, Bound, Closed };
enum class AxisType { Equidistant, Variable };


AxisType defines whether an axis is fully determined by $x_\text{min}$, $x_\text{max}$ and $N_\text{bins}$, or if there are variable bin boundaries $x_i = {x_1, \ldots x_N}$. The axis boundaries and, if necessary, the bin boundaries are provided in the constructor.

If at all possible, Equidistant is preferrable, since bin $b$ can then be calculated in constant time for given $x$ as

$$b = \frac{\mathrm{floor}(x - x_\text{min})}{w} + 1$$

where $b \in {0, 1, \ldots N_\text{bins}}$. If the type is Variable, a search for the correct bin is performed over the bin boundaries.


AxisBoundaryType steers how out-of-bounds lookups are handled. There are three options:

The three different axis boundary types

  • Bound: out-of-bounds lookups resolve to the closest valid bin.

  • Open: out-of-bounds lookups resolve to dedicated underflow and overflow bins.

  • Closed: out-of-bounds lookups wrap-around to the other side of the axis.

Grid creation

The types of the axes have to be known at compile-time, since they are provided to the Grid as template parameters. Thus the number of dimensions $N$ of the Grid is also fixed at compile-time. The axes can be any combination of the aforementioned variations.

template <typename T, class... Axes> class Grid;

where T is the value type that is stored in each bin. Instances of each axis are then given to the Grid constructor as a tuple. This Axis instances hold the number of bins and the boundary values, which are runtime values.

The Grid performs a lookup by recursively visiting each axis and having it perform a lookup in the corresponding component of the input vector. The lookup methods are templated on the input vector type, which only has to support component access. Transforming in and out of the local Grid reference frame is not handled by the Grid.

Local vs global bin indices

The underlying data structure of the Grid is a one-dimensional std::vector<T>, where T is the template parameter determining the stored value type. The index of this vector is referred to as the global bin index. The logical index structure, where each global bin is addressed by an $N$-dimensional vector of integers is called local bin indices. The two can be converted into one another using

using index_t = std::array<size_t, N>;
index_t getLocalBinIndices(size_t bin) const;
size_t getGlobalBinIndex(const index_t& localBins) const;

The local bin indices are always defined from 1 to $N_\text{bins}$ for the respective axis. Bins 0 and $N_\text{bins} + 1$ address the underflow and overflow bins, which are always present, even if AxisBoundaryType != Open.

Finding neighbors

The Grid can determine the number of neighbors around a given bin. This done by first converting the global bin index to local bin indices, and then varying the local bin indices in each dimension. At this stage, each Axis gets to decide, which indices are considered neighbors, which differs depending on AxisBinningType. Open considers the underflow and overflow bins, while Bound does not. Closed considers bins on the other side of the axis. The resulting neighbor combination around bin B might look like

b x b 1 2 3
1 x 0,+1 x
2 -1,0 B +1,0 
3 x 0,-1 x

Here, the corner combinations are still missing (also true for $N>2$). This is then turned into a hypercube which in $N=2$ results in:

b x b 1 2 3
1 -1,+1 0,+1 +1,+1
2 -1,0 B +1,0 
3 -1,-1 0,-1 +1,-1

These local bin indices are then converted into global bin indices before being returned.