Table of contents
1.
Introduction
2.
Map Classes
2.1.
Map Types and Declaration
2.2.
Using Map Variables
2.3.
Updating the Mapped Array
3.
Aliasing
3.1.
Examples
3.2.
Resolving Issues
3.3.
Component-wise Operations
3.4.
Matrix Multiplication
4.
Frequently Asked Questions
4.1.
What is a Positive Definite Matrix?
4.2.
How do we find the Adjoint of a Matrix?
4.3.
Explain trace().
4.4.
What are the applications of Eigen’s Library?
5.
Conclusion
Last Updated: Mar 27, 2024
Medium

Map classes and Aliasing

Author Rupal Saluja
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

Do you know why Eigen is gaining popularity nowadays?

The reasons are many, but we will learn a few prominent ones. Being open-source, it is accessible to all. It requires codes in C++, one of the most used programming languages. It works very fast and finds applications in many tasks. It transforms tedious calculations into feasible solutions. You can use Eigen’s library in Linear Algebra, Matrix Operations, Numericals, Algorithms, etc.

logo of Eigen

This article will discuss two of Eigen's most necessary foundational concepts. That is Map Classes and Aliasing and several modules of it. Every module has been explained using detailed examples to help you understand better.

Map Classes

Sometimes there arises a situation where you want to use raw arrays as an Eigenvector or matrix. You are given two choices- make a copy of the data or import the data. Which one would you choose? Obviously, the first one because you can reuse the memory that way. This can be extremely useful in numerous contexts. And fortunately, importing becomes very convenient when you use Map Classes. The term ‘Map’ suggests that you pick a certain part of data and paste it somewhere else.

Representation of map classes

Map Types and Declaration

Every Map Object has a type defined by its Eigen equivalent. The default type requires only one template parameter whose syntax is:

Map<Matrix<typename Scalar, int Rows, int Cols> >


You need two pieces of information to construct any Map variable.

  • The data you want to map in the form of an array, vector, or matrix.
  • A pointer to the memory region where you want to map the copied data.

For example, if you want to map a matrix of datatype ‘int’ with sizes defined at the compile time, the syntax will be:

Map<MatrixXi> mint(pi,rows,columns);


where pi is an int* pointing to the region where you want to map the copied data.

If you want to map a matrix of datatype ‘float’ with a pre-defined size, the syntax will be:

Map<const Vector4f> mf(pf);


where, pf is a float* pointing to the region where you want to map the copied data. In this case, you do not have to pass the size as a parameter in the constructor. This is because the size is already defined in the syntax.

Ensure that you pass a pointer to initialize any Map Variable or Object. This is because Map Class does not have a default constructor.

The Map class is flexible enough to form various data representations. The two template additional parameters provided with Map Classes are:

  • MapOptions

This parameter specifies if the pointer is Aligned or Unaligned. The default is set to Unaligned.

  • StrideType

This parameter is used when you want your output memory array in a customized layout. You have a complete set of Stride methods you can use as per need.

An example demonstrating both these parameters is given below.

int array[8];
for (int i = 0; i < 5; ++i) {
    arr[i] = i;
}
cout << "Column Major:\n" << Map < Matrix < int, 2, 3 > > (array) << endl;
cout << "Row Major using stride:\n" << Map < Matrix < int, 2, 3 > , Unaligned, Stride < 1, 3 > > (array) << endl;

 

Output:

Column Major
0 2 4
1 3 5
Row Major using stride:
0 1 2
3 4 5

Using Map Variables

Map Variables or Objects are used like any other Eigen Variable or Object. All in-built Eigen functions are designed to allow Map Variables or Objects like other Eigen Variables or Objects.

Use the example below for a better understanding.

typedef Matrix < fint, 1, Dynamic > MatrixType;
typedef Map < MatrixType > MapType;
typedef Map <
    const MatrixType > MapTypeConst;
const int n = 4;
MatrixType mone(n), mtwo(n);
mone.setRandom();
mtwo.setRandom();
int * p = & mtwo(0);
MapType mtwomap(p, mtwo.size());
MapTypeConst mtwomapconst(p, mtwo.size());
cout << mone << endl;
cout << mtwo << endl;
cout << "Squared distance: " << (mone - mtwo).squaredNorm() << endl;
cout << "Squared distance using map: " << (mone - mtwomap).squaredNorm() << endl;
mtwomap(2) = 7;
cout << "Updated: " << mtwo << endl;

 

Output:

68 21 56 59 82
60 33 53 44 10
Squared distance: 4.52
Squared distance using map: 4.52
Updated: 60 33 7 44 10


However, this does not happen automatically when you write your functions using Eigen’s library. This is because any Map type is not similar to its Dense equivalent. To read in detail about this, refer to the next module of this blog.

Updating the Mapped Array

You can update the array of already declared Map objects. For that, you have to use C++ ‘placement new’ syntax. There are two cases in which you can use this syntax.

  • When you know the location of the Mapped Array

It changes the appearance of the array but does not invoke the memory allocator. This is because you already know the location where you want to store the result array. 

You can use the example below for a better understanding.

int data[] = {
  1,
  2,
  3,
  4,
  5,
  6,
  7
};
Map < RowVectorXi > v1(data, 3);
cout << v1 << "\n";
new( & v1) Map < RowVectorXi > (data + 3, 5);
cout << v << "\n";


Output:

1 2 3
4 5 6 7

 

  • When you don’t know the location of a Mapped Array

It changes the appearance of the array and also invokes the memory allocator. This is because the syntax has yet to specify the location where you want to store the result array. 

You can use the example below for a better understanding.

Map < Matrix3f > A(NULL);
VectorXf b(n);
for (int i = 0; i < n; i++) {
    new( & A) Map < Matrix3f > (get_matrix_pointer(i));
    b(i) = A.trace();
}

Aliasing

Aliasing is a special condition of the assignment statement in which the matrix, array, or vector on LHS is similar to RHS's. 

For example,

matrix = matrix * n;
Or, matrix = matrix.transpose();

Aliasing can be very useful or extremely useless depending on the type of aliasing performed. Sometimes, it may give unexpected results. In the further modules, we will use different cases to explain when it can be harmless or harmful.

Aliasing

Examples

We will start with a simple example representing aliasing. Use the C++ code below for a better understanding.

MatrixXi matrix(3,3);
matrix << 11, 12, 13,   14, 15, 16,   17, 18, 19;
cout << matrix << endl;
matrix.bottomRightCorner(2,2) = matrix.topLeftCorner(2,2);
cout << "After aliasing: \n" << matrix << endl;

 

Output:

11 12 13
14 15 16
17 18 19
After Aliasing: 
11 12 13
14 11 12
17 14 11


Did you realize the output is far from expected? The problem is in this statement:

matrix.bottomRightCorner(2,2) = matrix.topLeftCorner(2,2);

 

Surely, this example represents aliasing. However, there is an error because of Eigen’s lazy evaluation. The coefficient matrix(1,1), which appears in both the blocks we used, disappears after aliasing. Logically, the coefficient matrix (1,1) should have shifted to the matrix (2,2), but that does not happen. The result we get is similar to the following:

matrix(1,1) = matrix(0,0);
matrix(1,2) = matrix(0,1);
matrix(2,1) = matrix(1,0);
matrix(2,2) = matrix(1,1);


The coefficient matrix(2,2) has been given a new value instead of an old one. Sometimes, it happens that aliasing needs to be detected at compile-time. Eigen detects some instances but not all. Still, no worries, Eigen’s library has a solution for this problem. The function eval() is used to solve this issue. We will learn in detail about resolving issues in the next topic of this module.

Now, we will use two examples to explain a basic characteristic of Aliasing. The statements:

vector = vector.head(i), and matrix = matrix.block(m,n,r,c)

represents aliasing. It shows aliasing occurs more efficiently when we shrink a matrix. Shrinking means reducing the size by cutting down some coefficients in any way.

Here is one more example, but a complex one. It represents Aliasing in Matrix and vector arithmetic.

Matrix2i n;
n << 10, 20, 30, 40;
cout << n << endl;
n = n.transpose();
cout << After aliasing:\n" << n << endl;


Output:

10 20
30 40
After aliasing:
10 20
20 40

 

Here comes another aliasing issue. However, this can be detected by Eigen using run-time assertion. It gives the output but exits with the following message with no resolution.

void Eigen::DenseBase < Derived > ::checkTransposeAliasing(const OtherDerived & ) 
const [with OtherDerived = Eigen::Transpose < Eigen::Matrix < int, 2, 2, 0, 2, 2 > > , Derived = Eigen::Matrix < int, 2, 2, 0, 2, 2 > ]: Assertion`(!internal::check_transpose_aliasing_selector<Scalar,internal::blas_traits<Derived>::IsTransposed,
OtherDerived>::run(internal::extract_data(derived()), other)) 
&& "aliasing detected during transposition, use transposeInPlace() 
or evaluate the rhs into a temporary using .eval()"' failed.

 

This issue is resolved by framing the EIGEN_NO_DEBUG macro turned on. The above example is illustrated with this property turned off to make you acknowledge the issue.

Resolving Issues

Now that you have understood the issues with Aliasing, it won’t be a problem for you to resolve them. We can conclude from the above two examples that Eigen has to use a temporary matrix to avoid using new values. It will evaluate the right-hand side completely and assign all the values to a temporary matrix. This temporary matrix values are assigned again to the left-hand side matrix. Eigen’s library has a function for this, eval().

The correct version of the first example above, using eval() functions, will be:

MatrixXi matrix(3,3);
matrix << 11, 12, 13,   14, 15, 16,   17, 18, 19;
cout << matrix << endl;
matrix.bottomRightCorner(2,2) = matrix.topLeftCorner(2,2).eval();
cout << "After aliasing: \n" << matrix << endl;

 

Output:

11 12 13
14 15 16
17 18 19
After Aliasing: 
11 12 13
14 11 12
17 14 11


Now, the output matrix appears as it should.

The same solution, the eval() function, resolves the issue for the next example. The correct version of the second example above, using eval() functions, will be:

Matrix2i n;
n << 10, 20, 30, 40;
cout << n << endl;
n = n.transpose().eval();
cout << After aliasing:\n" << n << endl;


Output:

10 20
30 40
After aliasing:
10 20
20 40


However, we have a better solution in Eigen’s library. There is a special multi-purpose function, transposeInPlace(), to replace any matrix with its transpose. It is the best to use, if available. This is because it optimizes the operation and shows clearly what we are doing. It is illustrated below.

MatrixXf n(2,3);
n << 10, 20, 30, 40, 50, 60;
cout << n << endl;
n.transposeInPlace();
cout << After transposing :\n" << n << endl;


Output:

10 20 30
40 50 60
After transposing:
10 40
20 50
30 60

 

Use the table below to know more about such replaceable functions.

Original Function InPlace() Function
MatrixBase::adjoint() MatrixBase::adjointInPlace()
DenseBase::reverse() DenseBase::reverseInPlace()
LDLT::solve() LDLT::solveInPlace()
LLT::solve() LLT::solveInPlace()
TriangularView::solve() TriangularView::solveInPlace()
DenseBase::transpose() DenseBase::transposeInPlace()

Component-wise Operations

You must have understood from the examples above that you will not get the expected results if the same matrix is on both sides of the assignment operator. Such operations are considered harmful to Aliasing. However, component-wise operations such as Scalar Multiplication, Matrix Addition, and Array Multiplication are safe.

Use the example below to understand component-wise operations better. As these operations are harmless, there is no need to use eval() function even if the same matrix is present on both sides of the assignment operator.

MatrixXf matrix(2, 2);
matrix << 1, 2, 3, 4;
cout << matrix << endl;
matrix = 4 * matrix;
cout << "After Multiplication: \n" << matrix << endl;
matrix = matrix - MatrixXf::Identity(3, 3);
cout << "After Subtraction: \n" << matrix << endl;
ArrayXXf array = matrix;
array = array.square();
cout << "After Squaring: \n" << array << endl;
//Combining all operations in one statement:
matrix << 1, 2, 3, 4;
matrix = (4 * matrix - MatrixXf::Identity(3, 3)).array().square();
cout << "Final Result: \n" << matrix << endl;

 

Output:

1 2
3 4
After Multiplication:
4    8
12 16
After Subtraction:
1 5
9 13
After Squaring:
1    25
81 169
Final Result:
1    25
81 169


We can say that Aliasing is safe when both sides of the assignment depend only on each other and not on any other entries.

Matrix Multiplication

Matrix Multiplication is the only Eigen operation that performs Aliasing by itself if the result matrix is not introduced to any changes. All other Eigen operations consider that there is no aliasing problem. This is mainly because it is a component-wise operation or the result matrix has been changed.

For Example,

MatrixXf mat(2,2);
matrix << 3, 0,  0, 3;
matrix = matrix * matrix;
cout << matrix;

 

Output:

9 0
0 9


However, this comes with a drawback. When executing matrix * matrix, Eigen uses a temporary space to store the product. This product is assigned to a matrix, which is fine. But what happens when you assign this product to a different variable?

Eigen still uses a temporary space which increases space complexity. It is more efficient to store the product directly to the new variable instead of using temporary space.

No worries, Eigen has a solution for this problem: noalias() function. Using this function, the user can mention when to perform aliasing. 

For Example,

matrix2.noalias() = matrix1 * matrix1

 

This will directly store the product to a new variable instead of copying it. Use the example to understand this better.

MatrixXf matrix1(2,2), matrix2(2,2);
matrix1 << 3, 0,  0, 3;
matrix2 = matrix1 * matrix1;
cout << matrix2 << endl << endl;
matrix2.noalias() = matrix1 * matrix2;
cout << matrix2;


Output:

9 0
0 9

9 0
0 9


Ensure you do not use noalias() property when Aliasing occurs. It may lead to unexpected results.

Frequently Asked Questions

What is a Positive Definite Matrix?

A Positive Definite Matrix is a symmetric matrix with every eigenvalue being positive.

How do we find the Adjoint of a Matrix?

To find the matrix's adjoint, we first find that matrix's cofactor. Then transpose it by assigning rows to columns and vice versa.

Explain trace().

The trace() is a pre-defined method of Eigen’s library. It is used to calculate the sum of diagonal elements of the matrix, known as the trace of the matrix.

What are the applications of Eigen’s Library?

Eigen’s library is extensively used for Robotics and some other real-world applications. You can easily perform a wide spectrum of algebraic computations using this library.

Conclusion

To cut it short, we understood Map Classes and their various aspects: types, declaring variables, using them, and changing the map array. We also went through Aliasing and its major points: examples, resolving issues, component-wise operations, and matrix multiplication. 

We hope the above discussion helped you understand the perks of Eigen’s Map Classes and Aliasing and can be used for future reference whenever needed. To learn more about Eigen and its components, you can refer to blogs on Eigen's GeometryEigen's Class HierarchyPreprocessor Tokens recognized by EigenAdvanced Methods for initializing Matrices, and Introduction to Sparse Matrix.

Visit our website to read more such blogs. Make sure you enroll in our courses, take mock tests, solve problems, and interview puzzles. Also, you can pay attention to interview stuff- interview experiences and an interview bundle for placement preparations. Do upvote our blog to help fellow ninjas grow.

Happy Coding!

Live masterclass