Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to use Eigen versus Lapack to solve equations #15

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions .github/workflows/validate-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ jobs:
strategy:
matrix:
#os: [ubuntu-20.04, ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-20.04, ubuntu-latest]
use_boost: ["false", "true"]
os: [ubuntu-20.04, ubuntu-latest, windows-latest]

steps:
- name: Fetch sources
Expand Down Expand Up @@ -47,16 +46,32 @@ jobs:
key: ${{ runner.os }}-meson-cache
path: 3rdparty/packagecache/

- name: Setup compile cache to reduce compile time
uses: hendrikmuhs/[email protected]
with:
key: ${{ runner.os }}-ccache

- name: Resolve C++ build dependencies (non-Windows)
run: >-
meson setup
-Duse_boost=${{ matrix.use_boost }}
-Duse_boost=false
build/
if: startsWith(matrix.os, 'window') == false

#- name: Resolve C++ build dependencies (msvc toolchain)
# run: meson setup --vsenv build/
# if: startsWith(matrix.os, 'window') == true
- name: Resolve C++ build dependencies (msvc toolchain)
run: >-
meson setup
-Darmadillo-code:lapack=none
-Duse_boost=false
-Duse_eigen=true
build/
#run: >-
# meson setup
# --vsenv
# -Darmadillo-code:lapack=none
# -Duse_eigen=true
# build/
if: startsWith(matrix.os, 'window') == true

- name: Build everything
#run: meson compile -C build/
Expand Down
9 changes: 5 additions & 4 deletions 3rdparty/armadillo-code.wrap
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[wrap-git]
url = https://gitlab.com/conradsnicta/armadillo-code.git
revision = 14.0.2
depth = 1
[wrap-file]
directory = armadillo-code-14.0.2
source_url = https://gitlab.com/conradsnicta/armadillo-code/-/archive/14.0.2/armadillo-code-14.0.2.tar.gz
source_filename = armadillo-code-14.0.2.tar.gz
source_hash = a067616d88af8d993fce1c73e1ebcf66aa57551a8bc6ba5b7f62b7b99991d207

patch_directory = armadillo-code
diff_files = 0001-Patch-armadillo-to-support-besselj.patch
Expand Down
13 changes: 13 additions & 0 deletions 3rdparty/eigen.wrap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[wrap-file]
directory = eigen-3.4.0
source_url = https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.bz2
source_filename = eigen-3.4.0.tar.bz2
source_hash = b4c198460eba6f28d34894e3a5710998818515104d6e74e5cc331ce31e46e626
patch_filename = eigen_3.4.0-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/eigen_3.4.0-2/get_patch
patch_hash = cb764fd9fec02d94aaa2ec673d473793c0d05da4f4154c142f76ef923ea68178
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/eigen_3.4.0-2/eigen-3.4.0.tar.bz2
wrapdb_version = 3.4.0-2

[provide]
eigen3 = eigen_dep
5 changes: 4 additions & 1 deletion 3rdparty/packagefiles/armadillo-code/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ project('armadillo', 'cpp',
# Header-only library
armadillo_inc = include_directories('include')

if get_option('lapack') == 'openblas64'
if get_option('lapack') == 'none'
warning('Disabled Lapack dependencies. SVD and LU algorithms will be undefined.')
lapack_dep = dependency('', required: false)
elif get_option('lapack') == 'openblas64'
lapack_dep = dependency('openblas64')
elif get_option('lapack') == 'openblas'
lapack_dep = dependency('openblas')
Expand Down
1 change: 1 addition & 0 deletions 3rdparty/packagefiles/armadillo-code/meson.options
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ option('lapack',
'openblas',
'lapack64',
'lapack',
'none',
],
value: 'openblas',
)
3 changes: 2 additions & 1 deletion besselj_armadillo_support/meson.build
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
armadillo_besselj_support_inc = include_directories('inc')
armadillo_dep = subproject('armadillo-code').get_variable('armadillo_dep')
armadillo_proj = subproject('armadillo-code')
armadillo_dep = armadillo_proj.get_variable('armadillo_dep')

# TODO(Antony): use "cxx.has_function" when it supports std namespace.
if get_option('use_boost')
Expand Down
9 changes: 9 additions & 0 deletions examples/generate-psf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

#include "make_psf.h"

namespace {
#ifdef LSTSQ_USE_LAPACK
constexpr bool has_lapack = true;
#else
constexpr bool has_lapack = false;
#endif
} // namespace

int main() {
using arma::hdf5_name;
using microsc_psf::makePSF;
Expand All @@ -20,6 +28,7 @@ int main() {
precision_li2017_t precision{};
precision.num_basis = 153;
precision.rho_samples = 1000;
precision.solver = has_lapack ? microsc_psf::SandersonAndCurtin2020 : microsc_psf::EigenBdcSVD;

const auto psf =
makePSF(params, {0.1_um, 0.25_um}, {256, 128}, 0.610_um, precision);
Expand Down
5 changes: 4 additions & 1 deletion examples/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ endif

generate_psf_exe = executable('generate-psf',
sources: 'generate-psf.cpp',
cpp_args: generate_psf_compile_args,
cpp_args: [
generate_psf_compile_args,
use_lapack_args,
],
dependencies: [
microsc_psf_dep,
hdf5_dep,
Expand Down
11 changes: 10 additions & 1 deletion meson.options
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
option('install_examples', type: 'boolean', value: false)
option('use_boost', type: 'boolean', value: false)
option('use_boost',
description: 'Compute bessel function of the first kind with Boost::Math',
type: 'boolean',
value: false,
)
option('use_eigen',
description: 'Solve linear least squares with Eigen3 the matrix library',
type: 'boolean',
value: false,
)
23 changes: 23 additions & 0 deletions microsc-psf/inc/linsolver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <armadillo>

namespace microsc_psf {
namespace internal {

/** Solve min || A * x - b ||^2 for x , using Eigen3 solver.
*
* Typical ussage: simulate PSF on machines not having LAPACK support, e.g.
* Windows on ARM64 CPU.
*
* Note: This may seem contrived to have two near-identical C++ libraries
* serving the linear algebra code, but Armadillo has a nice Matlab-like syntax
* that simplifies the code review process a lot.
*
* @tparam tranpose_b True if the signal b needs a Hermitian transpose ahead of
* the least squares solver.
*/
template <bool transpose_b>
arma::cx_mat solveWithEigen(const arma::mat& A, const arma::cx_mat& b);
} // namespace internal
} // namespace microsc_psf
10 changes: 8 additions & 2 deletions microsc-psf/inc/make_psf.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ enum linear_solver_option_t : uint8_t {
// https://arma.sourceforge.net/armadillo_solver_2020.pdf
SandersonAndCurtin2020,

// Penrose pseudo inverse.
PenroseInverse
/** Penrose pseudo inverse. */
PenroseInverse,

/** Eigen's SVD solver.
*
* Reference: https://eigen.tuxfamily.org/dox/classEigen_1_1BDCSVD.html
*/
EigenBdcSVD,
};

struct precision_li2017_t {
Expand Down
35 changes: 35 additions & 0 deletions microsc-psf/meson.build
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
microsc_psf_inc = include_directories('inc')

if get_option('use_eigen')
warning('Solving linear least squares with Eigen3, not Lapack')

# Eigen3 is a direct competitor of Armadillo library. This may seem
# contrived to require two nearly identicial libraries to compute PSF, but
# Armadillo has a Matlab-like syntax that simplifies the code review process
# a lot.
eigen_solver_lib = static_library('eigen_solver',
sources: 'src/linsolver.cpp',
include_directories: include_directories('inc'),
dependencies: [
dependency('eigen', fallback: ['eigen', 'eigen_dep']),
armadillo_dep,
],
)

eigen_solver_dep = declare_dependency(
link_with: eigen_solver_lib,
include_directories: include_directories('inc'),
compile_args: '-DLSTSQ_USE_EIGEN',
)
else
eigen_solver_dep = []
endif

lapack_dep = armadillo_proj.get_variable('lapack_dep')
if lapack_dep.found()
use_lapack_args = '-DLSTSQ_USE_LAPACK'
else
use_lapack_args = []
endif

microsc_psf_lib = static_library('microsc-psf',
sources: [
'src/main.cpp',
],
cpp_args: [
'-D' + bessel_source,
use_lapack_args,
],
include_directories: [
microsc_psf_inc,
Expand All @@ -13,6 +47,7 @@ microsc_psf_lib = static_library('microsc-psf',
dependencies: [
armadillo_dep,
boost_math_dep,
eigen_solver_dep,
],
)

Expand Down
53 changes: 53 additions & 0 deletions microsc-psf/src/linsolver.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "linsolver.h"

#include <Eigen/Dense>
#include <cassert>

namespace microsc_psf {

namespace internal {

template <bool transpose_b>
arma::cx_mat
solveWithEigen(const arma::mat& A_buffer, const arma::cx_mat& b_buffer) {
using arma::cx_double;
using Eigen::ComputeThinU;
using Eigen::ComputeThinV;
using Eigen::Map;
using Eigen::MatrixX;
using Eigen::MatrixXd;

const auto m = A_buffer.n_rows;
const auto n = A_buffer.n_cols;
const auto k = (transpose_b ? b_buffer.n_rows : b_buffer.n_cols);

if constexpr (transpose_b) {
assert(b_buffer.n_cols == m);
} else {
assert(b_buffer.n_rows == m);
}

// First, map to Eigen's primary data structure.
const Map<const MatrixXd> A(A_buffer.memptr(), m, n);
const Map<const MatrixX<cx_double>> b(b_buffer.memptr(), b_buffer.n_rows, b_buffer.n_cols);

// Allocate the result buffer
arma::cx_mat xopt_buffer(n, k);

// Next, solve with Eigen's SVD solver
Map<Eigen::MatrixX<cx_double>> xopt(xopt_buffer.memptr(), n, k);
if constexpr (transpose_b) {
// Eigen may have an efficient Hermitian transposed solver. Move the transpose step into
// Eigen.
xopt = A.bdcSvd(ComputeThinU | ComputeThinV).solve(b.transpose());
} else {
xopt = A.bdcSvd(ComputeThinU | ComputeThinV).solve(b);
}

return xopt_buffer;
}

template arma::cx_mat solveWithEigen<true>(const arma::mat&, const arma::cx_mat&);
template arma::cx_mat solveWithEigen<false>(const arma::mat&, const arma::cx_mat&);
} // namespace internal
} // namespace microsc_psf
39 changes: 34 additions & 5 deletions microsc-psf/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
// This must come after <boost/math/special_functions/bessel.hpp>
#include "make_psf.h"

#ifdef LSTSQ_USE_EIGEN
#include "linsolver.h"
#endif

namespace {

// Return the range [0, N), excluding N.
Expand Down Expand Up @@ -137,7 +141,7 @@ makePSF(microscope_params_t params, scale_t<Micron> voxel, scale_t<uint32_t> vol
{
// Define the basis of Bessel functions.
// Shape: number of basis function by number of rho samples.
auto&& J = besselj<0>(scaling_factor * Rho);
#define J besselj<0>(scaling_factor * Rho)

// Compute the approximation to the sampled pupil phase by finding the least squares
// solution to the complex coefficients of the Fourier-Bessel expansion.
Expand All @@ -146,10 +150,35 @@ makePSF(microscope_params_t params, scale_t<Micron> voxel, scale_t<uint32_t> vol
//
// Note: Armadillo does not have solver for real-valued matrix and complex-valued vector.
// Reference: https://arma.sourceforge.net/armadillo_solver_2020.pdf
cx_mat&& Ci = (precision.solver == SandersonAndCurtin2020)
? solve(conv_to<cx_mat>::from(J.t()), phase.t())
: cx_mat(pinv(J.t()) * phase.t());

cx_mat Ci;
switch (precision.solver) {
#ifdef LSTSQ_USE_LAPACK
case SandersonAndCurtin2020:
Ci = solve(conv_to<cx_mat>::from(J.t()), phase.t());
break;
case PenroseInverse:
Ci = pinv(J.t()) * phase.t();
break;
#else
case SandersonAndCurtin2020:
case PenroseInverse:
throw std::invalid_argument(R"(LAPACK not found.
Re-configure the C++ project with
"meson configure -Darmadillo-code:lapack=..." and try again.)");
break;
#endif

case EigenBdcSVD: {
#ifndef LSTSQ_USE_EIGEN
throw std::invalid_argument(R"(Eigen::BdcSVD solver not implemented.
Re-configure the C++ project with
"meson configure -Duse_eigen=true" and try again.)");
#else
constexpr bool always_transpose_phase = true;
Ci = microsc_psf::internal::solveWithEigen<always_transpose_phase>(J.t(), phase);
#endif
}
}
const cx_mat ciEle = Ele * Ci;
PSF0 = real(ciEle % conj(ciEle));
}
Expand Down