NOTE / 4/28/2020
Solving $Ax=b$ with Givens Rotations
Many engineering problems reduce to solving , where is a matrix and a column vector. QR, LLT, and other decompositions can solve it. This article introduces Givens rotations, used in SLAM algorithms such as MSCKF and iSAM.
1. Solving with QR decomposition
Before introducing Givens rotations, consider QR decomposition. Matrix can be decomposed into orthogonal matrix and upper-triangular matrix :
Then
Multiply both sides by :
Taking the upper part gives
Because is upper triangular, solve from the final row upward by back substitution.
This approach needs a QR decomposition and storage of . Givens rotations produce (3) without explicitly forming .
2. Givens rotation
The goal is to zero elements below the upper triangle of .
2.1 Two-dimensional case
For
zero by multiplying on the left by a Givens rotation matrix:
The rotation must satisfy
Since and are known, the Givens matrix is known and transforms to upper-triangular form. In practice, calculate sine and cosine directly with a numerically stable method rather than calculating .
For , multiply both sides by the same Givens matrix:
so
Back substitution gives and .
2.2 Overdetermined case
For
zero , , and $a_32 in order. Start at the bottom of the leftmost column and move right.
First, use to zero :
Next, use to zero :
Finally, use to zero :
This transforms into an upper-triangular matrix. The product of the three rotations is :
2.3 Summary
- Process columns from left to right.
- Within each column, process from bottom to top.
- Each Givens matrix is determined by the element to eliminate and its corresponding diagonal element.
QR decompositions are not unique. Different results from Givens rotations, MATLAB, or Eigen are not necessarily wrong. If diagonal matrix has entries or , then and define another QR decomposition.
3. Implementation
// Note: i is col, j is row.
void GivensCosSin(const double aii, const double aji, double* c, double* s) {
if (std::abs(aji) < 1e-12) {
*c = 1.;
*s = 0.;
} else if (std::abs(aji) > std::abs(aii)) {
const double aii_over_aji = aii / aji;
const double one_over_sqrt = 1. / std::sqrt(1. + aii_over_aji * aii_over_aji);
*c = - aii_over_aji * one_over_sqrt;
*s = one_over_sqrt;
} else {
const double aji_over_aii = aji / aii;
const double one_over_sqrt = 1. / std::sqrt(1 + aji_over_aii * aji_over_aii);
*c = one_over_sqrt;
*s = - aji_over_aii * one_over_sqrt;
}
}
// A * x = b to R * x = d.
void GivensRotationQRInPlace(Eigen::MatrixXd* A, Eigen::VectorXd* b) {
Eigen::MatrixXd& AA = *A;
Eigen::VectorXd& bb = *b;
const int rows = AA.rows();
const int cols = AA.cols();
assert(rows > cols);
assert(rows == bb.rows());
// Traverse all columns
for (int col = 0; col < cols; ++col) {
// Bottom to up.
for (int row = rows - 1; row > col; --row) {
// Compute cos_theta, sin_theta
double c, s;
GivensCosSin(AA(col, col), AA(row, col), &c, &s);
// Porcess A.
// For all elements at the ith, and jth rows.
for (int n = col; n < cols; ++n) {
const double a = AA(col, n);
const double b = AA(row, n);
AA(col, n) = c * a - s * b;
AA(row, n) = s * a + c * b;
}
// Process b.
{
const double a = bb(col);
const double b = bb(row);
bb(col) = c * a - s * b;
bb(row) = s * a + c * b;
}
}
}
}
bool BackSubstitution(const Eigen::MatrixXd& R, const Eigen::VectorXd& d, Eigen::VectorXd* x) {
x->resize(R.rows());
// From bottom to up.
for (int row = R.rows() - 1; row >= 0; --row) {
if (std::abs(R(row, row)) < 1e-12) {
return false;
}
double tmp = 0.;
for (size_t col = row + 1; col < R.cols(); ++col) {
tmp += R(row, col) * (*x)(col);
}
(*x)(row) = (d(row) - tmp) / R(row, row);
}
return true;
}
bool SolveLinearSystemInPlace(Eigen::MatrixXd* A, Eigen::VectorXd* b, Eigen::VectorXd* x) {
// Ax=b to Rx=d.
GivensRotationQRInPlace(A, b);
// Solve Rx = d.
return BackSubstitution(A->topRows(A->cols()), b->topRows(A->cols()), x);
}
4. References
- Joan Sola. Course on SLAM.
- Timothy Sauer. Numerical Analysis.
- Michael Kaess. iSAM: Incremental Smoothing and Mapping.
- G. Golub and C. V. Loan. Matrix Computations, 3rd ed.