Program Listing for File linalg.hpp
↰ Return to documentation for file (src/utils/linalg.hpp)
//
// Created by Zack Williams on 27/11/2020.
//
#ifndef UW12_LINALG_HPP
#define UW12_LINALG_HPP
#include "config.hpp"
#ifdef USE_ARMA
#include <armadillo>
#elif USE_EIGEN
#include <Eigen/Dense>
#include <fstream>
#include <iostream>
#include <random>
#include <unsupported/Eigen/MatrixFunctions> // mat.exp()
#else
#error
#endif
#include <cassert>
#include <cmath>
#include <vector>
// TODO: Consider moving ARMA/EIGEN to cpp
namespace uw12::linalg {
#ifdef USE_ARMA
using Mat = arma::mat;
using Vec = arma::vec;
#elif USE_EIGEN
typedef Eigen::MatrixXd Mat;
typedef Eigen::VectorXd Vec;
#else
#error
#endif
inline size_t n_elem(const Vec &vec) {
#ifdef USE_ARMA
return vec.n_elem;
#elif USE_EIGEN
return vec.size();
#else
#error
#endif
}
inline size_t n_elem(const Mat &mat) {
#ifdef USE_ARMA
return mat.n_elem;
#elif USE_EIGEN
return mat.size();
#else
#error
#endif
}
inline size_t n_rows(const Mat &mat) {
#ifdef USE_ARMA
return mat.n_rows;
#elif USE_EIGEN
return mat.rows();
#else
#error
#endif
}
inline size_t n_cols(const Mat &mat) {
#ifdef USE_ARMA
return mat.n_cols;
#elif USE_EIGEN
return mat.cols();
#else
#error
#endif
}
inline Vec vec(const size_t n_el) {
#ifdef USE_ARMA
return arma::vec(n_el);
#elif USE_EIGEN
return Vec(n_el);
#else
#error
#endif
}
inline Vec vec(const std::vector<double> &vector) {
#ifdef USE_ARMA
return vector;
#elif USE_EIGEN
const auto ptr = const_cast<double *>(vector.data());
const auto n = vector.size();
return Eigen::Map<Vec>(ptr, n);
#else
#error
#endif
}
inline Vec ones(const size_t n_el) {
#ifdef USE_ARMA
return arma::ones(n_el);
#elif USE_EIGEN
return Vec::Ones(n_el);
#else
#error
#endif
}
inline Vec zeros(const size_t n_el) {
#ifdef USE_ARMA
return arma::zeros(n_el);
#elif USE_EIGEN
return Vec::Zero(n_el);
#else
#error
#endif
}
inline double elem(const Vec &vec, const size_t index) {
#ifdef USE_ARMA
return vec(index);
#elif USE_EIGEN
return vec(index);
#else
#error
#endif
}
inline Mat mat(
double *mem, const size_t n_row, const size_t n_col, const bool copy = false
) {
#ifdef USE_ARMA
return {mem, n_row, n_col, copy, true}; // CheckMem
#elif USE_EIGEN
if (copy) {
const Mat mat1 = Eigen::Map<Mat>(mem, n_row, n_col);
const Mat mat2 = mat1;
return mat2;
}
return Eigen::Map<Mat>(mem, n_row, n_col);
#else
#error
#endif
}
inline double elem(
const Mat &mat, const size_t row_index, const size_t col_index
) {
#ifdef USE_ARMA
return mat(row_index, col_index);
#elif USE_EIGEN
return mat(row_index, col_index);
#else
#error
#endif
}
inline void set_elem(
Mat &mat, const size_t row_index, const size_t col_index, const double value
) {
if (row_index >= n_rows(mat) || col_index >= n_cols(mat)) {
throw std::logic_error("Index outside scope of matrix");
}
mat(row_index, col_index) = value;
}
inline void set_elem(Vec &vec, const size_t index, const double value) {
if (index >= n_elem(vec)) {
throw std::logic_error("Index outside scope of vector");
}
vec(index) = value;
}
inline Mat mat(const size_t n_row, const size_t n_col) {
#ifdef USE_ARMA
return arma::mat(n_row, n_col);
#elif USE_EIGEN
return Mat(n_row, n_col);
#else
#error
#endif
}
inline Mat ones(const size_t n_row, const size_t n_col) {
#ifdef USE_ARMA
return arma::ones(n_row, n_col);
#elif USE_EIGEN
return Mat::Ones(n_row, n_col);
#else
#error
#endif
}
inline Mat zeros(const size_t n_row, const size_t n_col) {
#ifdef USE_ARMA
return arma::zeros(n_row, n_col);
#elif USE_EIGEN
return Mat::Zero(n_row, n_col);
#else
#error
#endif
}
inline Mat id(const size_t n) {
#ifdef USE_ARMA
return arma::eye(n, n);
#elif USE_EIGEN
return Mat::Identity(n, n);
#else
#error
#endif
}
inline Mat transpose(const Mat &mat) {
#ifdef USE_ARMA
return mat.t();
#elif USE_EIGEN
return mat.transpose();
#else
#error
#endif
}
inline Mat random(const size_t n_row, const size_t n_col, const int seed) {
#ifdef USE_ARMA
arma::arma_rng::set_seed(seed);
return arma::randu(n_row, n_col);
#elif USE_EIGEN
std::default_random_engine generator(seed);
std::uniform_real_distribution<double> distribution(0, 1);
const auto uniform = [&] { return distribution(generator); };
const Mat mat = Mat::NullaryExpr(n_row, n_col, uniform);
return mat;
#else
#error
#endif
}
inline bool is_square(const Mat &mat) {
#ifdef USE_ARMA
return mat.is_square();
#elif USE_EIGEN
const auto n_row = n_rows(mat);
const auto n_col = n_cols(mat);
return n_row == n_col;
#else
#error
#endif
}
inline bool is_symmetric(const Mat &mat, const double threshold = 1e-10) {
#ifdef USE_ARMA
return mat.is_symmetric(threshold);
#elif USE_EIGEN
const auto n = n_rows(mat);
if (n != n_cols(mat)) {
return false;
}
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < i; j++) {
if (std::abs(mat(i, j) - mat(j, i)) > threshold) {
return false;
}
}
}
return true;
#else
#error
#endif
}
inline bool all_positive(const Vec &vec) {
#ifdef USE_ARMA
return all(vec > 0);
#elif USE_EIGEN
return (vec.array() > 0).all();
#else
#error
#endif
}
inline Vec schur(const Vec &vec1, const Vec &vec2) {
if (n_elem(vec1) != n_elem(vec2)) {
throw std::logic_error(
"Cannot calculate Schur product for vectors of different sizes"
);
}
#ifdef USE_ARMA
return vec1 % vec2;
#elif USE_EIGEN
return vec1.cwiseProduct(vec2);
#else
#error
#endif
}
inline double dot(const Mat &mat1, const Mat &mat2) {
const auto n_col = n_cols(mat1);
if (n_col != n_cols(mat2)) {
throw std::logic_error("Matrices must have same number of columns");
}
if (n_rows(mat1) != n_rows(mat2)) {
throw std::logic_error("Matrices must have same number of rows");
}
#ifdef USE_ARMA
return arma::dot(mat1, mat2);
#elif USE_EIGEN
double result = 0;
for (size_t i = 0; i < n_col; ++i) {
result += mat1.col(i).dot(mat2.col(i));
}
return result;
#else
#error
#endif
}
inline Mat reshape(
const Mat &mat,
const size_t n_row,
const size_t n_col,
const bool copy_data = false
) {
if (n_row * n_col != n_elem(mat)) {
throw std::logic_error(
"Reshaping a matrix must preserve total number of elements"
);
}
#ifdef USE_ARMA
constexpr bool strict = true;
return {
const_cast<double *>(mat.memptr()), n_row, n_col, copy_data, strict
}; // CheckMem
#elif USE_EIGEN
if (!copy_data) {
return Eigen::Map<Mat>(const_cast<double *>(mat.data()), n_row, n_col);
} else {
auto mat2 = mat;
mat2.resize(n_row, n_col);
return mat2;
}
#else
#error
#endif
}
inline Mat reshape_col(
const Mat &mat,
const size_t col_idx,
const size_t n_row,
const size_t n_col,
const bool copy_data = false
) {
if (n_row * n_col != n_rows(mat)) {
throw std::logic_error(
"Reshaped column of a matrix must preserve number of elements"
);
}
if (col_idx >= n_cols(mat)) {
throw std::logic_error("Column index must be in matrix");
}
#ifdef USE_ARMA
constexpr bool strict = true;
return {
const_cast<double *>(mat.colptr(col_idx)), n_row, n_col, copy_data, strict
}; // CheckMem
#elif USE_EIGEN
if (!copy_data) {
return Eigen::Map<Mat>(
const_cast<double *>(mat.col(col_idx).data()), n_row, n_col
);
} else {
Mat out = mat.col(col_idx);
out.resize(n_row, n_col);
return out;
}
#else
#error
#endif
}
inline Mat p_inv(const Mat &mat, const double threshold = 1e-10) {
#ifdef USE_ARMA
return pinv(mat, threshold);
#elif USE_EIGEN
auto decomposition = mat.completeOrthogonalDecomposition();
decomposition.setThreshold(threshold);
return decomposition.pseudoInverse();
#else
#error
#endif
}
inline Mat diagmat(const Vec &vec) {
#ifdef USE_ARMA
return arma::diagmat(vec);
#elif USE_EIGEN
return vec.asDiagonal();
#else
#error
#endif
}
inline Mat expmat(const Mat &mat) {
#ifdef USE_ARMA
return arma::expmat(mat);
#elif USE_EIGEN
return mat.exp();
#else
#error
#endif
}
inline Mat random_pd(const size_t n_row, const int seed) {
const Vec eigen = random(n_row, 1, seed) + ones(n_row);
const Mat X = random(n_row, n_row, seed);
const Mat X2 = 0.5 * (X - transpose(X));
const Mat U = expmat(X2);
return U * diagmat(eigen) * transpose(U);
}
inline Mat inv_sym_pd(const Mat &mat) {
#ifdef USE_ARMA
return inv_sympd(mat);
#elif USE_EIGEN
if (!is_square(mat)) {
throw std::logic_error("Matrix is not square");
}
return mat.inverse();
#else
#error
#endif
}
inline Vec sub_vec(const Vec &vec, const size_t row1, const size_t n_row) {
if (n_row > n_rows(vec)) {
throw std::logic_error("sub vec cannot be larger than parent vec");
}
if (row1 >= n_rows(vec)) {
throw std::logic_error("starting index cannot be outside parent vector");
}
if (row1 + n_row > n_rows(vec)) {
throw std::logic_error("Final index outside of parent vector range");
}
#ifdef USE_ARMA
return vec.subvec(row1, row1 + n_row - 1);
#elif USE_EIGEN
return vec.segment(row1, n_row);
#else
#error
#endif
}
inline Mat sub_mat(
const Mat &mat,
const size_t row1,
const size_t col1,
const size_t n_row,
const size_t n_col
) {
if (row1 >= n_rows(mat)) {
throw std::logic_error("Start row not in parent matrix");
}
if (col1 >= n_cols(mat)) {
throw std::logic_error("Start column not in parent matrix");
}
if (row1 + n_row > n_rows(mat)) {
throw std::logic_error("submatrix row index must be in parent index");
}
if (col1 + n_col > n_cols(mat)) {
throw std::logic_error("submatrix column index must be in parent index");
}
#ifdef USE_ARMA
return mat.submat(row1, col1, arma::size(n_row, n_col));
#elif USE_EIGEN
return mat.array().block(row1, col1, n_row, n_col);
#else
#error
#endif
}
inline double norm(const Mat &mat) {
#ifdef USE_ARMA
return arma::norm(mat);
#elif USE_EIGEN
const Eigen::JacobiSVD svd(mat);
return svd.singularValues().array().abs().maxCoeff();
#else
#error
#endif
}
inline Vec col(
const Mat &mat, const size_t col_idx, const bool copy_data = false
) {
if (col_idx >= n_cols(mat)) {
throw std::logic_error("column index must be in parent matrix");
}
#ifdef USE_ARMA
const auto n_row = n_rows(mat);
return reshape_col(mat, col_idx, n_row, 1, copy_data);
#elif USE_EIGEN
return mat.col(col_idx);
#else
#error
#endif
}
inline Mat row(const Mat &mat, const size_t row_idx) {
if (row_idx >= n_rows(mat)) {
throw std::logic_error("row index must be in parent matrix");
}
#ifdef USE_ARMA
return mat.row(row_idx);
#elif USE_EIGEN
return mat.row(row_idx);
#else
#error
#endif
}
inline Mat rows(const Mat &mat, const size_t row_idx, const size_t n_row) {
const auto n_col = n_cols(mat);
return sub_mat(mat, row_idx, 0, n_row, n_col);
}
inline Vec head(const Vec &vec, const size_t n_row) {
if (n_row > n_elem(vec)) {
throw std::logic_error(
"Cannot take more head rows from vector than total number of rows"
);
}
#ifdef USE_ARMA
return vec.head(n_row);
#elif USE_EIGEN
return vec.head(n_row);
#else
#error
#endif
}
inline Vec tail(const Vec &vec, const size_t n_row) {
if (n_row > n_elem(vec)) {
throw std::logic_error(
"Cannot take more tail rows from vector than total number of rows"
);
}
#ifdef USE_ARMA
return vec.tail(n_row);
#elif USE_EIGEN
return vec.tail(n_row);
#else
#error
#endif
}
inline Mat head_cols(
const Mat &mat, const size_t n_col, const bool copy_data = false
) {
const auto n_row = n_rows(mat);
if (n_col > n_cols(mat)) {
throw std::logic_error(
"Cannot have submatix with more columns than parent matrix"
);
}
#ifdef USE_ARMA
constexpr auto strict = true;
return {const_cast<double *>(mat.memptr()), n_row, n_col, copy_data, strict};
#elif USE_EIGEN
if (!copy_data) {
return Eigen::Map<Mat>(const_cast<double *>(mat.data()), n_row, n_col);
} else {
return mat.leftCols(n_col);
}
#else
#error
#endif
}
inline Mat tail_cols(
const Mat &mat, const size_t n_col, const bool copy_data = false
) {
if (n_col > n_cols(mat)) {
throw std::logic_error(
"Cannot have submatix with more columns than parent matrix"
);
}
const auto n_row = n_rows(mat);
const auto n_col_all = n_cols(mat);
assert(n_col_all >= n_col);
const auto col1 = n_col_all - n_col;
#ifdef USE_ARMA
constexpr auto strict = true;
return {
const_cast<double *>(mat.colptr(col1)), n_row, n_col, copy_data, strict
};
#elif USE_EIGEN
if (!copy_data) {
return Eigen::Map<Mat>(
const_cast<double *>(mat.col(col1).data()), n_row, n_col
);
} else {
return mat.rightCols(n_col);
}
#else
#error
#endif
}
inline Mat head_rows(const Mat &mat, const size_t n_row) {
if (n_row > n_rows(mat)) {
throw std::logic_error(
"Cannot have submatix with more rows than parent matrix"
);
}
#ifdef USE_ARMA
return mat.head_rows(n_row);
#elif USE_EIGEN
const auto n_col = n_cols(mat);
return sub_mat(mat, 0, 0, n_row, n_col);
#else
#error
#endif
}
inline Mat tail_rows(const Mat &mat, const size_t n_row) {
if (n_row > n_rows(mat)) {
throw std::logic_error(
"Cannot have submatix with more rows than parent matrix"
);
}
#ifdef USE_ARMA
return mat.tail_rows(n_row);
#elif USE_EIGEN
const auto n_col = n_cols(mat);
const auto n_row_all = n_rows(mat);
assert(n_row_all >= n_row);
const auto row1 = n_row_all - n_row;
return sub_mat(mat, row1, 0, n_row, n_col);
#else
#error
#endif
}
inline Vec vectorise(const Mat &mat) {
#ifdef USE_ARMA
return arma::vectorise(mat);
#elif USE_EIGEN // check memory
return Eigen::Map<Vec>(const_cast<double *>(mat.data()), n_elem(mat));
#else
#error
#endif
}
inline double trace(const Mat &mat) {
if (n_rows(mat) != n_cols(mat)) {
throw std::logic_error("Matrix not square");
}
#ifdef USE_ARMA
return arma::trace(mat);
#elif USE_EIGEN
return mat.trace();
#else
#error
#endif
}
inline Mat sqrt(const Mat &mat) {
for (size_t i = 0; i < n_cols(mat); ++i) {
if (const auto col = linalg::col(mat, i); !all_positive(col)) {
throw std::logic_error("Cannot compute square root of negative elements");
}
}
#ifdef USE_ARMA
return arma::sqrt(mat);
#elif USE_EIGEN
return mat.array().sqrt();
#else
#error
#endif
}
inline void assign_cols(Mat &mat, const Mat &input, const size_t offset) {
const auto n = n_cols(input);
if (offset >= n_cols(mat)) {
throw std::logic_error("offset greater than number of columns of mat");
}
if (n_rows(mat) != n_rows(input)) {
throw std::logic_error("Matrices have different number of rows");
}
if (n_cols(mat) < offset + n) {
throw std::logic_error("Columns outside boundary of matrix");
}
assert(n_cols(mat) >= offset + n - 1);
assert(n_rows(mat) == n_rows(input));
#ifdef USE_ARMA
mat.cols(offset, offset + n - 1) = input;
#elif USE_EIGEN
for (size_t i = 0; i < n; ++i) {
mat.col(offset + i) = input.col(i);
}
#else
#error
#endif
}
inline void assign_rows(Mat &mat, const Mat &input, const size_t offset) {
const auto n = n_rows(input);
if (offset >= n_rows(mat)) {
throw std::logic_error("offset greater than number of rows of mat");
}
if (n_cols(mat) != n_cols(input)) {
throw std::logic_error("Matrices have different number of columns");
}
if (n_rows(mat) < offset + n) {
throw std::logic_error("Rows outside boundary of matrix");
}
#ifdef USE_ARMA
mat.rows(offset, offset + n - 1) = input;
#elif USE_EIGEN
for (size_t i = 0; i < n; ++i) {
mat.row(offset + i) = input.row(i);
}
#else
#error
#endif
}
inline void assign_rows(Vec &vec, const Vec &input, const size_t offset) {
const auto n = n_elem(input);
if (offset >= n_elem(vec)) {
throw std::logic_error("offset greater thansize of vec");
}
if (n_elem(vec) < offset + n) {
throw std::logic_error("assignment outside boundary of vec");
}
#ifdef USE_ARMA
vec.rows(offset, offset + n - 1) = input;
#elif USE_EIGEN
for (size_t i = 0; i < n; ++i) {
vec(offset + i) = input(i);
}
#else
#error
#endif
}
inline const double *mem_ptr(const Vec &vec) {
#ifdef USE_ARMA
return const_cast<double *>(vec.memptr());
#elif USE_EIGEN
return vec.data();
#else
#error
#endif
}
inline const double *mem_ptr(const Mat &mat) {
#ifdef USE_ARMA
return const_cast<double *>(mat.memptr());
#elif USE_EIGEN
return mat.data();
#else
#error
#endif
}
inline bool empty(const Vec &vec) {
#ifdef USE_ARMA
return vec.empty();
#elif USE_EIGEN
return n_elem(vec) == 0;
#else
#error
#endif
}
inline bool empty(const Mat &mat) {
#ifdef USE_ARMA
return mat.empty();
#elif USE_EIGEN
return n_elem(mat) == 0;
#else
#error
#endif
}
inline double max_abs(const Vec &vec) {
#ifdef USE_ARMA
return max(arma::abs(vec));
#elif USE_EIGEN
return vec.array().abs().maxCoeff();
#else
#error
#endif
}
inline bool nearly_equal(
const Mat &mat1, const Mat &mat2, const double epsilon
) {
#ifdef USE_ARMA
return approx_equal(mat1, mat2, "both", epsilon, epsilon);
#elif USE_EIGEN
return mat1.array().isApprox(mat2.array(), epsilon);
#else
#error
#endif
}
inline Mat each_col(const Mat &mat, const Vec &vec) {
if (n_elem(vec) != n_rows(mat)) {
throw std::logic_error("size of vector does not match matrix");
}
#ifdef USE_ARMA
return mat.each_col() % vec;
#elif USE_EIGEN
return mat.array().colwise() * vec.array();
#else
#error
#endif
}
inline Mat join_matrices(const Mat &mat1, const Mat &mat2) {
if (n_rows(mat1) != n_rows(mat2)) {
throw std::logic_error("matrices do not have the same number of rows");
}
#ifdef USE_ARMA
return join_horiz(mat1, mat2);
#elif USE_EIGEN
const auto n_row = n_rows(mat1);
assert(n_rows(mat2) == n_row);
const auto n_col_1 = n_cols(mat1);
const auto n_col_2 = n_cols(mat2);
Mat out(n_row, n_col_1 + n_col_2);
out.leftCols(n_col_1) = mat1;
out.rightCols(n_col_2) = mat2;
return out;
#else
#error
#endif
}
inline Mat load_csv(const std::string &filepath) {
#ifdef USE_ARMA
Mat A;
const bool load_ok = A.load(arma::csv_name(filepath));
if (!load_ok) {
throw std::runtime_error("cannot read data from file");
}
return A;
#elif USE_EIGEN
std::ifstream in_data;
in_data.open(filepath);
std::string line;
std::vector<double> values;
uint rows = 0;
while (std::getline(in_data, line)) {
std::stringstream lineStream(line);
std::string cell;
while (std::getline(lineStream, cell, ',')) {
values.push_back(std::stod(cell));
}
++rows;
}
return Eigen::Map<Mat>(values.data(), values.size() / rows, rows).transpose();
#else
#error
#endif
}
inline std::pair<Vec, Mat> eigen_system(const Mat &mat) {
if (!is_square(mat)) {
throw std::logic_error("matrix must be square");
}
if (!is_symmetric(mat)) {
throw std::logic_error("matrix must be symmetric");
}
Vec vals;
Mat vecs;
#ifdef USE_ARMA
const auto success = arma::eig_sym(vals, vecs, mat);
if (!success) {
throw std::runtime_error("eigen-decomposition failed");
}
#elif USE_EIGEN
Eigen::SelfAdjointEigenSolver<Mat> eigen_solver(mat);
if (eigen_solver.info() != Eigen::Success) {
throw std::runtime_error("eigen-decomposition failed");
}
vals = eigen_solver.eigenvalues();
vecs = eigen_solver.eigenvectors();
#else
#error
#endif
assert(n_cols(vecs) == n_elem(vals));
assert(n_rows(vecs) == n_rows(mat));
return {vals, vecs};
}
inline std::pair<Vec, Mat> eigen_decomposition(
const Mat &matrix,
const double linear_dependency_threshold,
const double eigen_ld_threshold
) {
const auto [vals, vecs] = eigen_system(matrix);
if (linear_dependency_threshold > 0 || eigen_ld_threshold > 0) {
const auto max_eig = max_abs(vals);
const auto threshold =
std::max(max_eig * eigen_ld_threshold, linear_dependency_threshold);
// Throw away vectors with zero eigenvalues
#ifdef USE_ARMA
const arma::uvec non_zero = find(arma::abs(vals) > threshold);
const arma::vec eigenvalues = vals.rows(non_zero);
const arma::mat eigenvectors = vecs.cols(non_zero);
#elif USE_EIGEN
const Eigen::VectorXi non_zero =
(vals.array().abs() > threshold).cast<int>();
const auto n = non_zero.sum();
Vec eigenvalues(n);
Mat eigenvectors(vecs.rows(), n);
int idx = 0;
for (int i = 0; i < vals.size(); ++i) {
if (non_zero[i]) {
eigenvalues(idx) = vals(i);
eigenvectors.col(idx) = vecs.col(i);
idx++;
}
}
#else
#error
#endif
return {eigenvalues, eigenvectors};
}
return {vals, vecs};
}
} // namespace uw12::linalg
#endif // UW12_LINALG_HPP