Alin Andersen
4371 字
22 分钟
飞行器动力学-P2_Eigen库入门

Eigen是C++下一个广泛应用的线性代数库,本文对其主要用法进行记录,以备使用.
运行环境
本文使用CMake自行搭建编译环境,使用VSCode进行代码编辑.
软件 | 版本号 |
---|---|
CMake | 4.0.0 |
Eigen | 3.4.0 |
基本数据类型
- Eigen中的基本数据类型为Matrix,Vector与Array;
- 所有Matrix/Vector本质上都是Matrix类,Vector是其中单行/单列的特殊情况,支持部分额外操作(如[]运算符);Array类型在部分操作略有不同(如乘法).
// 1.标准定义// 矩阵:三个参数分别为值类型(如double),行数与列数.后两者必须在编译期给定.// 向量:同理,只是行数/列数至少有一个为1.Matrix<typename Scalar, int RowsAtCompileTime, int ColsAtCompileTime>Array<typename Scalar, int RowsAtCompileTime, int ColsAtCompileTime>// 扩展定义:Options == 0:列优先(默认), == RowMajor:行优先// MaxRowsAtCompileTime:指定动态数组最大行数; MaxColsAtCompileTime列数同理.Matrix<typename Scalar, int RowsAtCompileTime, int ColsAtCompileTime, int Options = 0, int MaxRowsAtCompileTime = RowsAtCompileTime, int MaxColsAtCompileTime = ColsAtCompileTime>Array<typename Scalar, int RowsAtCompileTime, int ColsAtCompileTime, int Options = 0, int MaxRowsAtCompileTime = RowsAtCompileTime, int MaxColsAtCompileTime = ColsAtCompileTime>
// 2.Dynamic参数// 可用于替代行数/列数/同时替代两者,表示该值在运行时给定.// 以下定义均合法.// 注意:对元素数量<16的情况,动态分配(堆分配)会显著降低性能.Matrix<double, Dynamic, Dynamic> m1;Matrix<double, Dynamic, 1> m2;Matrix<double, 3, Dynamic> m3;Array<double, Dynamic, Dynamic> a1;Array<double, Dynamic, 1> a2;Array<double, 3, Dynamic> a3;
// 3.矩阵基本成员函数// 行数,列数与元素总个数.Matrix<double, 3, 3> m;Array<double, 3, 3> m;m.rows();m.cols();m.size();a.rows();a.cols();a.size();
// 4.初始化// 默认初始化:不会将元素置0(置0初始化参见zero()函数).Matrix<int, 3, 3> m;Array<double, Dynamic, Dynamic> a;// 动态数组的reserve,首参数为行Matrix<double, Dynamic, Dynamic> m(10,15); // 10*15矩阵Matrix<double, Dynamic, 1> v(30); // 30*1向量Array<double, Dynamic, Dynamic> a(10,15);// (C++11)使用初始化列表Matrix<int, 3, 1> v{1, 2, 3};Array<int, 3, 1> a{1, 2, 3};// 使用<<运算符Matrix<int, 4, 4> m;Matrix<int, 2, 2> m2;Matrix<int, 3, 1> v1, v2;Matrix<int, 6, 1> v3;Array<int, 3, 3> a;m2 << 1,2,3,4; // 等价于{{1,2}, {3,4}};m << m2, m2, m2, m2; // 合法,次序同上一行(行优先填充)a << 1,2,3,4,5,6,7,8,9;v3 << v1, v2; // 合法// 相同大小的Array和Matrix在赋值时可以直接互换.Matrix<int, 3, 3> m;Array<int, 3, 3> a;a = m; // 合法
// 5.下标访问// 从0开始,使用(), 第一个参数是行.// 对Matrix,Vector和Array都支持.m(0, 0) = 3;v(0) = 3;a(0, 0) = 3;v[2] = 3; // []运算符只允许用在vector上
// 6.Resize// 仅适用于Dynamic类.// 6.1 .resize() 不保证操作后矩阵元素仍有效.m.resize(3, 4);v.resize(5);// 6.2 .conservativeResize() 保证操作后矩阵元素仍有效// 缩小:保留原矩阵左上角的元素.// 扩大:多出来的元素未初始化.m.conservativeResize(3, 4);// 6.3 Assign and resizingMatrix<double, Dynamic, Dynamic> m2(2, 2), m3(3, 3);m2 = m3;assert(m2.rows == 3 && m2.cols == 3);
// 7.类型别名// 以下定义中, N == 2, 3, 4, X(Dynamic);// t == i(int), f(float), d(double), cf(std::complex<float>), cd(std::complex<double>)using MatrixNt = Matrix<type, N, N>; // 如Matrix3d = Matrix<double, 3, 3>;using MatrixN1N2t = Matrix<type, N1, N2>; // 如MatrixX3d = Matrix<double, 3, dynamic>;using VectorNt = Matrix<type, N, 1>;using RowVectorNt = Matrix<type, 1, N>;using ArrayNt = Array<type, N, N>;using ArrayN1N2t = Array<type, N1, N2>;
基本操作
介绍Matrix/Array的基本运算操作.
NOTE行优先&列优先
计算机内的内存空间是一维的,要将二维矩阵存储其中有两种方式.
行优先:顾名思义,优先存储同一行内的元素.第二行的第一个元素存储在第一行的最后一个元素之后;
列优先:优先存储同一列内的元素.第二列的第一个元素存储在第一列的最后一个元素之后;
Eigen默认使用列优先模式.将科学计算中最常用的列向量进行连续存储,方便提升缓存命中率/进行SIMD优化,加速计算.
Matrix运算
/* Ext. Eigen中的运算时机 为提高效率,Eigen采用"懒求值"模式,也即使用各类运算符时,运算不会立即发生; 相反,Eigen会返回一个表达式对象,实际计算在值实际被用到时才发生. "用到"值的最典型场景是将运算结果赋值给一个对象,也即使用operator=. 有时会导致数据竞争,具体运算符条目中有说明.*/MatrixXi mat(3, 3);mat << 1, 2, 3, 4, 5, 6, 7, 8, 9;// 危险!对矩阵(1,1)点同时存在读取与写入操作,存在数据竞争.mat.bottomRightCorner(2, 2) = mat.topLeftCorner(2, 2);// 使用eval()函数显式生成中间变量,消除竞争.mat.bottomRightCorner(2, 2) = mat.topLeftCorner(2, 2).eval();
// 1. 加法与减法// 相关运算符: 单目运算符-; 双目运算符+ -; 复合运算符+= -=.// 前提1:两对象的行数与列数对应// 前提2:两对象的元素类型相同(自动类型提升不可用)// 前提3:两侧运算对象均为array或均为matrixMatrix2d m1;Matrix3d m2;Matrix2f m3;Array22d a1, a2;auto res = m1 + m2; // 不合法,违反原则1auto res = m1 + m3; // 不合法,违反原则2auto res = m1 + a1; // 不合法,违反原则3// 以下运算正确.Matrix2d m4, m5;m4 + m5;m4 += m5;m4 - m5;m4 -= m5;-m4;m4 + a1.matrix();
// 2. 数乘与数除// 以下运算均合法.Matrix m1;double d;m1 * d;d * m1;m1 *= d;m1 / d;m1 /= d;
// 3. 转置与共轭// 注意懒求值:a=a.transpose()会带来数据竞争,计算结果可能与预期不符.Matrix3d m;m.transpose(); // 转置(有数据竞争)m.transposeInPlace(); // 原位转置(无数据竞争)m.conjugate(); // 共轭m.adjoint(); // 共轭转置(有数据竞争)m.adjointInPlace(); // 原位共轭转置(无数据竞争)
// 4. 矩阵乘法// 这是Eigen中唯一一种默认数据竞争会发生的情况.// 如果实际上不会,可以在左侧添加.noalias()以提高性能.Matrix3d m1, m2, m3;m1 * m2;m1 *= m2; // == (m1 = m1 * m2)m3.noalias() += m1 * m2; // 如果确保不会发生数据竞争
// 5. 点乘与叉乘Eigen::Vector3d v(1, 2, 3);Eigen::Vector3d w(0, 1, 2);v.dot(w);v.cross(w); // 对2维向量也可用,用以计算行列式
// 6. 基本算数操作Eigen::Matrix3d m, m2;Eigen::Vector3d v;std::ptrdiff_t i, j;m.sum(); // 所有元素的和m.prod(); // 所有元素的积m.mean(); // 所有元素的均值m.minCoeff(&i, &j); // 所有元素中的最小值(i,j是可选入参,表示位置)v.minCoeff(&i); // i是可选入参,表示位置m.maxCoeff(); // 所有元素中的最大值m.trace(); // 矩阵的迹m.cwiseProduct(m2); // m与m2的逐元素乘法,等效于(m.array() * m2.array()).matrix()m.squaredNorm(); // 模的平方m.norm(); // 模m.normalize(); // m原位归一化m.normalized(); // m非原位归一化m.lpNorm<n>(); // n范数,(((m所有元素的绝对值)的n次方之和)的(1/n)次方.比如Norm是2范数.m.lpNorm<Eigen::infinity>(); // +inf范数,m绝对值最大的元素.m.sin(); // 逐元素sin(弧度制输入)m.cos();m.all(); // 返回true如果m元素全为真.(a > 0).all(); // 合法.m.any(); // 返回true如果m元素存在真.m.count(); // 返回真元素数量.
// 7.向array转换,无运行时开销.auto a = m.array();
Array运算
// Array的大部分运算符与Matrix通用// 区别在于Array间相乘时,返回逐元素积而非矩阵积.Eigen::ArrayXXf a(2, 2);Eigen::ArrayXXf b(2, 2);a << 1, 2, 3, 4;b << 5, 6, 7, 8;auto c = a * b; // c == {{5, 12}, {21, 32}};
// 2. Array的基本算数操作Eigen::Array33d a;a.abs(); // 返回|a|a.sqrt(); // 逐元素开根号a.min(a2);// 每个元素是a和a2中较小的一个
// 3.向Matrix转换,无运行时开销.auto m = a.matrix();
矩阵分块&Slicing
- 矩阵分块零运行时开销.
// 视返回值的动/静态类型,分块方法略有不同.// 注意:两种方法控制的是返回值类型,m矩阵本身动态/静态均可.// 分块可以用作左值或右值.Eigen::matrix4d m;Eigen::vector4d v;auto m2 = m.block(1,1,2,2); // m2是matrixXXd类型,以(1,1)为左上角取2x2的块auto m3 = m.block<2,2>(1,1);// 分块大小编译期确定;m3是matrix22d类型,内容与m2一致.m.block<2,2>(0,0) = m2; // 合法.m.block(0,0,2,2) = m3; // 合法.v.head(n); // 矢量的头n个元素.v.head<n>();v.tail(n); // 尾n个元素.v.tail<n>();v.segment(i, n); // 从i开始的n个元素v.segment<n>(i);
// 2.特殊分块函数// 按行/按列/按角落分,比使用block方便高效.// 2.1. 按行列分// 返回值动/静态类型视m矩阵的动/静态类型.Eigen::matrix4d m;Eigen::matrixXd md;m.row(i); // 第i行,从0开始.静态矩阵.m.col(j); // 第j列,从0开始.静态矩阵.md.row(i); // 内容与上面相同.但是返回值类型为动态矩阵.md.col(j);m.topRows(q); // 最上面q行m.topRows<q>();m.bottomRows(q); // 最下面q行m.bottomRows<q>();m.leftCols(p); // 左侧p列m.leftCols<p>();m.rightCols(p); // 右侧p列m.rightCols<p>();m.middleCols(i, q); // 从i列开始的q列m.middleCols<q>(i);m.middleRows(i, p); // 从i行开始的q行m.middleRows<p>(i);
// 2.2. 按角落分Eigen::matrix4d m;m.topLeftCorner(p, q);m.topLeftCorner<p, q>();m.bottomLeftCorner(p, q);m.bottomLeftCorner<p, q>();m.topRightCorner(p, q);m.topRightCorner<p, q>();m.bottomRightCorner(p, q);m.bottomRightCorner<p, q>();
// 2.3. 对角m.diagonal(); // (只适用方阵)取主对角元素,其余置为0.
// 3. Slicing, ()运算符的高级应用/* 除了取单个元素, m(p,q)中的p,q还可以接受多种类型的参数. * 本质要求:定义了<integral type> operator[](<integral type>) const; <integral type> size() const; 两个函数. * 其中<integral type>可以是整型或者Eigen::Index可接受的类型(如std::ptrdiff_t). * 常见形式如下: * int:最常见的形式,取1行/1列. * Eigen::placeholders::all: 取该矩阵所有行/所有列. * ArithmeticSequence: 由Eigen::seq, Eigen::seqN或Eigen::lastN生成的序列. * Eigen数据结构:Eigen::Vector, Eigen::Matrix(仅限1维), Eigen::Array(仅限1维). * 标准库/C风格数据结构: std::Array, std::vector, int[N]. */// 3.1. seq与seqN的定义.seq(firstIdx, lastIdx); // seq(2, 6) = {2, 3, 4, 5, 6}seq(firstIdx, lastIdx, incr); // seq(2, 6, 2) = {2, 4, 6}seqN(firstIdx, size); // seqN(2, 4) = {2, 3, 4, 5}seqN(firstIdx, size, incr); // seqN(2, 4, 2) = {2, 4, 6, 8}// 3.2. 取末位:lastN函数A(lastN(p), lastN(q)); // == A.bottomRightCorner(p, q);A(all, lastN(n,3)); // 取所有行,从最后一列开始,每隔2列取1列.// 3.3. 编译期尺寸计算: 可以结合Eigen::fix()实现,在seq, seqN, lastN函数中.v(seq(last-fix<7>, last-fix<2>);v(seqN(last-7, fix<6>));A(Eigen::placeholders::all, seq(fix<0>,last,fix<2>)); // A的所有偶数列(第一列为0列)// 3.4. 倒序: 使用.reverse()或者seq/seqN.A(seqN(last, n, fix<-1>), all);A(lastN(n).reverse(), all);// 3.5. 自定义下标列表// 只需支持以下函数/* Index s = ind.size(); or Index s = size(ind); * Index i; * i = ind[i]; */// 示例程序:padusing Eigen::Index;struct pad { Index size() const { return out_size; } Index operator[](Index i) const { return std::max<Index>(0, i - (out_size - in_size)); } Index in_size, out_size;};
Matrix3i A;A.reshaped() = VectorXi::LinSpaced(9, 1, 9);// reshaped还没讲,此时A为{{1, 4, 7} ,{2, 5, 8}, {3, 6, 9}}.MatrixXi B(5, 5);B = A(pad{3, 5}, pad{3, 5});/* B = * 1 1 1 4 7 * 1 1 1 4 7 * 1 1 1 4 7 * 2 2 2 5 8 * 3 3 3 6 9*/
高级操作
高级初始化
// 1. 预定义的高级Matrix/Array.// 三种初始化方式.Matrix3d::Zero(); // 0参数版本,适用于固定大小Matrix/Array.ArrayXd::Zero(2); // 1参数版本,适用于一维动态Matrix(Vector)/一维动态Array.ArrayXXd::Zero(row, col); // 2参数版本// 若未专门说明,三种方式均可以用于以下方法.// 方法列表.ArrayXXd::Zero(row, col); // 全0矩阵ArrayXXd::Constant(row, col, val); // 初值矩阵ArrayXXd::Random(row, col); // [0,1)随机初值ArrayXXd::Identity(row, col);// 单位矩阵VectorXd::Linspaced(size, low, high); // 线性排布,仅支持一维结构(size==1则返回high)// 原位变换版本..setZero();.setIdentity();.setLinspaced(size, low, high);// .finished()方法:基于"<<"运算符构建运算对象.mat = (MatrixXf(2, 2) << 0, 1, 1, 0).finished() * mat;// 2. 预定义的别名.Vector3d::UnitX();Vector3d::UnitY();Vector3d::UnitZ();
部分降阶&广播(Partial Reduction & Broadcasting)
// 1. 部分降阶.// 1.1. 按行/列降阶..colwise() // 按列降阶处理.rowwise() // 按行降阶处理// 1.2. 常见用法Eigen::Matrix3d m;m.colwise().maxCoeff(); // 返回1*3向量,表示每列最大元素.mat.colwise().sum().maxCoeff(&maxIndex); // 返回最大和列的序号
// 2. 广播.// 第二操作数必须是Vector(对Matrix)或一维Array(对Array).Eigen::MatriXd m(2, 4);Eigen::VectorXd v(2);mat.colwise() += v; // 将v加到m的每一列.(m.colwise() - v).colwise().squaredNorm().minCoeff(&index); // 综合运用.
变形(Reshape)
// Ex. 行优先与列优先
// Eigen 3.4: 添加reshaped(NRowsType,NColsType)函数,允许在原矩阵上创造视图.// 提示:使用eval()以避免数据竞争.Matrix4i m;m.reshaped(2, 8); // 按列取数,按列填充,(0,0)->(0,0),(1,0)->(1,0),(2,0)->(0,1), ...m.reshaped(); // 默认形成单列向量m.reshaped<RowMajor>(); // 按行取数,按列填充.m.reshaped<AutoOrder>();// 根据默认行/列优先设置进行填充.
// 如果希望in-place改变存储的数据,使用resize().Matrix4i m;m.resize(2, 8); // 根据默认行/列优先设置进行填充.
STL迭代器与算法
// 1D Vector/1D Array定义了随机访问迭代器.// 节选支持的功能:// Ranged-forVectorXi v;for (auto x : v) cout << x << " ";// std::sortstd::sort(v.begin(), v.end());
// 多维数组可以使用reshaped()创建一维视图.Matrix2i A;for (auto x : A.reshaped()) cout << x << " ";
// 使用rowwise()/colwise()配合使用.for (auto row : A.rowwise()) std::sort(row.begin(), row.end());
与C++数组的接口
/* 1. Map: 在C/C++数据结构上创建的Eigen视图 * MapOptions: 内存对齐Aligned/Unaligned(默认). * StrideType<OuterStrideAtCompileTime_ , * InnerStrideAtCompileTime_> * 其中Inner表示相邻元素间的指针增量,Outer表示相邻行/列间的指针增量. * 两者均可指定为Dynamic以在运行时赋值. */// 1.1. Map的基础定义Map<Matrix<...> >Map<Matrix<...>, int MapOptions, typename StrideType>Map<const Matrix<...> > // 只读Map// 1.2. 基础用法Map<MatrixXf> mf() = delete; // 禁止默认初始化Map<MatrixXf> mf(pf, rows, cols); // pf:首地址float*类型指针Map<const Vector4f> mi(pf);// 1.3. Stride的使用示例.// Stride<Dynamic, 2>(8, 2)表示Outer值为8,Inner值为2.// 此处采用列优先存储,故同一列相邻行间的元素距离为Inner,// 同一行相邻列之间的元素距离为Outer.int array[24];for (int i = 0; i < 24; ++i) array[i] = i;cout << Map<MatrixXi, 0, Stride<Dynamic, 2> >(array, 3, 3, Stride<Dynamic, 2>(8, 2)) << endl;/* res = * 0 8 16 * 2 10 18 * 4 12 20 */// 1.4. 修改Map对象指向的内存地址.// 使用C++ Placement new语法.int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};Map<RowVectorXi> v(NULL);new (&v) Map<RowVectorXi>(data + 4, 5); // 指向data+4位置长度为5的行向量.
以Eigen类型为参数的函数
- 为了使”懒求值”可以继续,需要以特殊方式书写函数.
- 相对而言比较复杂,在性能不敏感场景下建议使用C++风格数组传值.
// 1. 使用模板函数.// 1.1. Eigen的所有运算对象都是少数基模板类的派生.Eigen::MatrixBase // 所有稠密矩阵运算对象的基类,用于仅矩阵函数.Eigen::ArrayBase // 所有稠密数组运算对象的基类,用于仅数组函数.Eigen::DenseBase // 上述两者的共同基类,用于适用于两者的函数.Eigen::EigenBase // 进一步包含特殊矩阵,稀疏矩阵等所有运算对象.
// 1.2. 示例函数// 输出给定矩阵的逆条件数.template <typename Derived>void print_inv_cond(const MatrixBase<Derived>& a){ const typename JacobiSVD<typename Derived::PlainObject>::SingularValuesType& sing_vals = a.jacobiSvd().singularValues(); std::cout << "inv cond: " << sing_vals(sing_vals.size()-1) / sing_vals(0) << std::endl;}// 以下函数获取Array对象的最大元素.template <typename Derived>void print_max_coeff(const ArrayBase<Derived> &a){ std::cout << "max: " << a.maxCoeff() << std::endl;}// 以下函数获取稠密运算对象的矩阵块.template <typename Derived>void print_block(const DenseBase<Derived>& b, int x, int y, int r, int c){ std::cout << "block: " << b.block(x,y,r,c) << std::endl;}// 以下函数可以获取对象b的行列与总元素数量信息.template <typename Derived>void print_size(const Eigen::EigenBase<Derived>& b) { std::cout << "size (rows, cols): " << b.size() << " (" << b.rows() << ", " << b.cols() << ")" << std::endl;}// 多输入参数情况,需要给定不同的DeriveType.template <typename DerivedA,typename DerivedB>typename DerivedA::Scalar squaredist(const MatrixBase<DerivedA>& p1,const MatrixBase<DerivedB>& p2){ return (p1-p2).squaredNorm();}
// 2. 基于Eigen::ref非模板函数.// 以下函数可以接受MatrixXf或其子块为实参.float inv_cond(const Eigen::Ref<const Eigen::MatrixXf>& a) { const Eigen::VectorXf sing_vals = a.jacobiSvd().singularValues(); return sing_vals(sing_vals.size() - 1) / sing_vals(0);}
// 3. 在函数中resize数据对象// 参考https://libeigen.gitlab.io/eigen/docs-nightly/TopicFunctionTakingEigenTypes.html,使用const_cast去掉const引用,过于黑魔法了,最好别用.
空间坐标系变换
Eigen通过Transform类提供两种变换.
- 抽象变换(Abstract Transformations),包括旋转,translations,缩放变换.
- 投影仿射(Projective&Affine).
其中仿射变换暂时用不到,只介绍抽象变换中的旋转变换.
// 1. 常用变换.// 1.1. 抽象旋转定义.Rotation2D<float> rot2(angle_in_radian); // 2D旋转AngleAxis<float> aa(angle_in_radian, Vector3f(ax,ay,az)); // 3D旋转,转轴必须是单位向量.Quaternion<float> q(0.1, 0.2, 0.3, 0.4); // 四元数.// 1.2. 常用别名.using Rotation2Df = Rotation2D<float>;using Rotation2Dd = Rotation2D<double>;using AngleAxisf = AngleAxis<float>;using AngleAxisd = AngleAxis<double>;using Quaternionf = Quaternion<float>;using Quaterniond = Quaternion<double>;// 1.3. 转换关系.// 只要维度,内部存储类型(即scalar类型)对应,各种类型间可以任意互转.Rotation2Dd angle_axis_2;AngleAxisd angle_axis_3(angle, Vector3D::UnitZ());Quaterniond q4;Matrix2d m2;Matrix3d m3;m2 = angle_axis_2; angle_axis_2 = m2;m3 = angle_axis_3; angle_axis_3 = m3;m3 = q4; q4 = m3;angle_axis_3 = q4; q4 = angle_axis_3 = q4;// 3. 变换方法.vector3d v3, res;AngleAxisd a1, a2, a3; // 假设两者已经有合适定义.a3 = a1 * a2; // 连续变换,先a2后a1.res = a3 * v3; // 进行变换.a2 = a1.inverse(); // 反变换.// slerp:球面线性插值// 找到rot1与rot2中,由[0,1]间的插值因子t定义的中间变换.// rot1与rot2需要为Rotation2D或四元数类.rot3 = rot1.slerp(t,rot2);// 4. 转欧拉角.// 参数:使用3维变换的成员函数,例如(2,1,0)表示321转换.// 返回值:三次旋转角度的弧度制表示,第一次旋转对应res[0].// 网上部分帖子中提到,有时转换会出现角度>PI的现象.Matrix3d m3;AngleAxisd a;Eigen::Vector3d res = m3.eulerAngles(2,1,0); // 对旋转矩阵使用Eigen::Vector3d res = a.eulerAngles(2,1,0);
飞行器动力学-P2_Eigen库入门
https://www.lithium-hydroxide.space/posts/250714_eigen_tutorial/