Field and ArrayView

In the C++ SDK, channel data inside a LidarFrame is represented with Field, a contiguous multidimensional container.

A field is described by a FieldDescriptor which describes the memory stored by the field and FieldClass, an enumeration giving LidarFrame some information about how to categorize the field.

On the python side, fields are directly mapped to NumPy arrays so this document will only cover the C++ side.

Constructing a Field

This section describes constructing a standalone field. To construct a field inside the LidarFrame, refer to LidarFrame documentation.

The simplest pattern is to describe an array with fd_array (or a FieldDescriptor) and pass it to the Field constructor:

// templated version
Field field(fd_array<uint32_t>(128, 1024));
// ChanFieldType version
Field field_cft(fd_array(ChanFieldType::UINT32, 128, 1024));

Type restrictions

Standalone fields support any primitive and plain-old-data types when used directly. The mechanism that provides type safety when accessing the data inside fields, however, is not cross platform so for serialization purposes we mirror commonly used field types with ChanFieldType. Fields inside LidarFrame should only be stored with types available with ChanFieldType.

Newly created fields are zeroed out over the entire content of the field. Neither constructors nor destructors are called for stored elements, so fields are best suited for storage of primitive types.

Accessing data

Fields are not designed to provide data access directly. To facilitate that, we use a conversion mechanism instead - fields support converting to multiple wrapper classes and check type and dimensional validity when the conversion happens, otherwise throwing std::invalid_argument.

As Eigen

One- and two-dimensional fields can be accessed as Eigen arrays:

Field field(fd_array<uint32_t>(128, 1024));
Eigen::Ref<img_t<uint32_t>> img = field;

As ArrayView

Alternatively, data can be accessed as ArrayView without Eigen’s dimension limits:

Field field(fd_array<uint32_t>(128, 1024));
ArrayView<uint32_t, 2> array_view = field;

As pointer

Data can also be directly accessed as a pointer of respective type (in such case, no dimension checks are made):

Field field(fd_array<uint32_t>(128, 1024));
uint32_t* ptr = field;

ArrayView

ArrayView is a lightweight non-owning view over multidimensional data. The class’s main motivation is to provide slicing functionality similar to that of NumPy ndarrays and avoid dimensional limitations imposed by Eigen matrices.

Typical sources include implicit conversion from a Field / FieldView, or constructing directly from T* and shape.

Read-only access uses ConstArrayView (an alias for ArrayView<const T, Dim>). Convenience aliases ArrayView1ArrayView4 (and matching ConstArrayView*) name common ranks.

Element access

Scalar reads and writes use the operator() with exactly Dim integer indices, outer dimension first, the same convention as indexing an Eigen::Array with a(i, j, …). Arity is enforced at compile time. Out-of-bound indices produce undefined behavior (there is no runtime check).

std::vector<float> data(100 * 100);
ArrayView2<float> img(data.data(), {100, 100});
const int row = 10;
const int col = 20;
float x = img(row, col);  // read
img(row, col) = 3.14f;    // write

Slicing and subview

subview mirrors NumPy-style indexing:

std::vector<int> data(100 * 100 * 100);
ArrayView3<int> cube{data.data(), {100, 100, 100}};
// Equivalent to NumPy arr[10]
ArrayView2<int> square_slice = cube.subview(10);
// Equivalent to NumPy arr[10, 20]
ArrayView1<int> line_slice = cube.subview(10, 20);

Calling subview with keep() keeps the full extent of that axis without changing rank.

std::vector<int> data(100 * 100 * 100);
ArrayView3<int> cube{data.data(), {100, 100, 100}};
const int fixed_row = 10;
const int fixed_col = 20;
// Equivalent to NumPy arr[:, fixed_row, fixed_col]
ArrayView1<int> line = cube.subview(keep(), fixed_row, fixed_col);

Calling subview with keep(start, end) slices the extent of the axis with half-open range [start, end):

std::vector<int> data(100 * 100);
ArrayView2<int> matrix{data.data(), {100, 100}};
// Equivalent to NumPy arr[:, 10:50]
ArrayView2<int> band = matrix.subview(keep(), keep(10, 50));

reshape

reshape(dims...) returns a new ArrayView over the same data range with a potentially different compile-time rank and a new shape. After reshaping, indexing follows the usual compact layout for the new dimensions. Reshaping only works for non-sparse views and will throw an std::invalid_argument error otherwise.

std::vector<int> data(12);
ArrayView1<int> flat{data.data(), {12}};
// Same storage, new rank: 12 -> 3 x 4 row-major, then 2 x 2 x 3
ArrayView2<int> mat = flat.reshape(3, 4);
ArrayView3<int> box = mat.reshape(2, 2, 3);

FieldView

FieldView is a non-owning equivalent of Field, allowing the use of the slicing and reshaping functionality of ArrayView before converting into a concrete data accessor.

Field and FieldView share the same implicit conversions (typed pointer, Eigen, ArrayView). For usage and type rules, see Accessing data.

Slicing and subview

FieldView::subview uses the same slicing model as ArrayView slicing. Each argument is either an index (that dimension is fixed and dropped from the logical rank) or a slice descriptor. subview returns a new FieldView sharing the same allocation.

Example:

std::vector<int> data(100 * 100);
FieldView view(data.data(), fd_array<int>(100, 100));
FieldView column = view.subview(keep(), 42);

reshape

For FieldView, reshape(dims...) follows the same model as ArrayView reshape: it keeps the same underlying data pointer. Sparse views cannot be reshaped.

std::vector<int> data(12);
FieldView view(data.data(), fd_array<int>(3, 4));
// Same allocation and element count; descriptor gets a new shape + row-major strides
FieldView box = view.reshape(2, 2, 3);