Arpack是一个优秀的大型稀疏矩阵特征值求解开源库,其基于blas和lapack提供了高效的特征值求解方案。其他还有勉强的可替代方案如mkl,SLEPc,Spectra,Eigen等,这些库也提供了特征值求解功能,但是通常在效率和可并行性上不如arpack。
Arpack基于 Arnoldi 方法从一个给定的稀疏矩阵中提取部分特征值和特征向量。Arnoldi 方法通过逐步构造一个正交基,并在每次迭代时逼近矩阵的特征空间,从而实现高效的特征计算。调用Arpack可以通过C语言或者Fortran语言进行。
现在通常使用的版本叫Arpack-ng,这个版本在github上的地址为:
https://github.com/opencollab/arpack-ng
在之前的文章现代先进构建工具Cmake-使用Cmake构建C++和Fortran中, 我们介绍了采用Cmake构建C++和Fortran工程,实际上,对于这里的Arpack库,我们的编译过程就主要通过Cmake配合相应的编译器完成。
本文主要介绍在Windows下如何编译这个库,生成.lib静态文件,从而可以实现在我们自己的代码中使用arpack库。
编译过程:
1.安装VS,OneApi,这里我安装的版本是VS2019,oneApi2025;
安装Cmake;
2.设置路径,下载代码,这里采用git从github上拉取代码,当然直接从github的网站上右键保存也是一样的。
3.在路径下创建一个build文件夹用于存编译文件;
4.打开Cmake-gui,设置生成的二进制文件路径为build,源代码路径为下载下来的arpack-ng的目录,然后点击Configure;
5.经过若干时间后,Configure完成,这里产生了一些红色的警告,大部分我们可以先不用关注,唯一关注的点是ICB:OFF这一项,如果希望通过C语言或者C++语言调用Arpack,那ICB是必须打开的,如果只需要用Fortran,哪ICB可以不用开。
6.按照下图方式分别勾选EXAMPLES,ICBA, INTERFACE64和TESTS,再次点击Configure,如果Configure成功,就点Generate;
7.显示“Generating done”以后,再点击Open Project,VS则打开对应的项目。点击生成-生成解决方案。
8.在之前的build路径下出现了“Debug”或者“Release”目录,其中有个静态库文件arpack.lib
9.这样实际上就完成了Arpack的编译,下面进行测试验证,新建C++项目,输入以下源代码:
/*
* This example demonstrates the use of C++ bindings to call arpack.
*
* Use arpack as you would have normally done, but, use [ae]upd instead
* of *[ae]upd_. The main advantage is that compiler checks the argument types
* and the correct function is called based on the type (float vs double vs
* complex). Note: to debug arpack, call debug_c. This is a test program to
* solve for the 9 eigenvalues of A*x = lambda*x where A is the diagonal
* matrix with entries 1000, 999, ... , 2, 1 on the diagonal.
*/
#include <cmath>
#include <iostream>
#include <vector>
#include "arpack.hpp"
#include "debug_c.hpp" // debug arpack.
#include "stat_c.hpp" // arpack statistics.
template <typename Real>
void diagonal_matrix_vector_product(const Real* x, Real* y) {
for (int i = 0; i < 1000; ++i) {
y[i] = static_cast<Real>(i + 1) * x[i];
}
}
template <typename Real>
void real_symmetric_runner(double const& tol_check, arpack::which const& ritz_option) {
const a_int N = 1000;
const a_int nev = 9;
const a_int ncv = 2 * nev + 1;
const a_int ldv = N;
const a_int ldz = N;
const a_int lworkl = ncv * (ncv + 8);
const a_int rvec = 1; // need eigenvectors
const Real tol = 0.000001; // small tol => more stable checks after EV computation.
const Real sigma = 0.0; // not referenced in this mode
std::vector<Real> resid(N);
std::vector<Real> V(ldv * ncv);
std::vector<Real> z(ldz * nev);
std::vector<Real> d(nev);
std::vector<Real> workd(3 * N);
std::vector<Real> workl(lworkl);
std::vector<a_int> select(ncv); // since HOWMNY = 'A', only used as workspace here
a_int iparam[11], ipntr[11];
iparam[0] = 1; // ishift
iparam[2] = 10 * N; // on input: maxit; on output: actual iteration
iparam[3] = 1; // NB, only 1 allowed
iparam[6] = 1; // mode
a_int info = 0, ido = 0;
do {
arpack::saupd(ido, arpack::bmat::identity, N,
ritz_option, nev, tol, resid.data(), ncv,
V.data(), ldv, iparam, ipntr, workd.data(),
workl.data(), lworkl, info);
diagonal_matrix_vector_product(&(workd[ipntr[0] - 1]), &(workd[ipntr[1] - 1]));
} while (ido == 1 || ido == -1);
// check info and number of ev found by arpack.
if (info < 0 || iparam[4] < nev) { /*arpack may succeed to compute more EV than expected*/
std::cout << "ERROR in saupd: iparam[4] " << iparam[4] << ", nev " << nev
<< ", info " << info << std::endl;
throw std::domain_error("Error inside ARPACK routines");
}
arpack::seupd(rvec, arpack::howmny::ritz_vectors, select.data(), d.data(),
z.data(), ldz, sigma, arpack::bmat::identity, N,
ritz_option, nev, tol, resid.data(), ncv,
V.data(), ldv, iparam, ipntr, workd.data(),
workl.data(), lworkl, info);
if (info < 0) throw std::runtime_error("Error in seupd, info " + std::to_string(info));
for (int i = 0; i < nev; ++i) {
Real val = d[i];
Real ref = static_cast<Real>(N - (nev - 1) + i);
Real eps = std::fabs(val - ref);
std::cout << val << " - " << ref << " - " << eps << std::endl;
/*eigen value order: smallest -> biggest*/
if (eps > tol_check) throw std::domain_error("Correct eigenvalues not computed");
}
std::cout << "------" << std::endl;
}
template <typename Real>
void diagonal_matrix_vector_product(const std::complex<Real>* x, std::complex<Real>* y) {
for (int i = 0; i < 1000; ++i) {
// Use complex matrix (i, -i) instead of (i, i): this way "largest_magnitude"
// and "largest_imaginary" options produce different results that can be checked.
y[i] = x[i] * std::complex<Real>{Real(i + 1), -Real(i + 1)};
}
}
template <typename Real>
void complex_nonsymmetric_runner(double const& tol_check, arpack::which const& ritz_option) {
const a_int N = 1000;
const a_int nev = 9;
const a_int ncv = 2 * nev + 1;
const a_int ldv = N;
const a_int ldz = N;
const a_int lworkl = ncv * (3 * ncv + 5);
const a_int rvec = 0; // eigenvectors omitted
const Real tol = 0.000001; // small tol => more stable checks after EV computation.
const std::complex<Real> sigma(0.0, 0.0); // not referenced in this mode
std::vector<std::complex<Real>> resid(N);
std::vector<std::complex<Real>> V(ldv * ncv);
std::vector<std::complex<Real>> z(ldz * nev);
std::vector<std::complex<Real>> d(nev);
std::vector<std::complex<Real>> workd(3 * N);
std::vector<std::complex<Real>> workl(lworkl);
std::vector<std::complex<Real>> workev(2 * ncv);
std::vector<Real> rwork(ncv);
std::vector<a_int> select(ncv); // since HOWMNY = 'A', only used as workspace here
a_int iparam[11], ipntr[14];
iparam[0] = 1; // ishift
iparam[2] = 10 * N; // on input: maxit; on output: actual iteration
iparam[3] = 1; // NB, only 1 allowed
iparam[6] = 1; // mode
a_int info = 0, ido = 0;
do {
arpack::naupd(ido, arpack::bmat::identity, N,
ritz_option, nev, tol, resid.data(), ncv,
V.data(), ldv, iparam, ipntr, workd.data(),
workl.data(), lworkl, rwork.data(), info);
diagonal_matrix_vector_product(&(workd[ipntr[0] - 1]), &(workd[ipntr[1] - 1]));
} while (ido == 1 || ido == -1);
// check info and number of ev found by arpack.
if (info < 0 || iparam[4] < nev) { /*arpack may succeed to compute more EV than expected*/
std::cout << "ERROR in naupd: iparam[4] " << iparam[4] << ", nev " << nev
<< ", info " << info << std::endl;
throw std::domain_error("Error inside ARPACK routines");
}
arpack::neupd(rvec, arpack::howmny::ritz_vectors, select.data(), d.data(),
z.data(), ldz, sigma, workev.data(), arpack::bmat::identity, N,
ritz_option, nev, tol, resid.data(), ncv,
V.data(), ldv, iparam, ipntr, workd.data(),
workl.data(), lworkl, rwork.data(), info);
if (info < 0) throw std::runtime_error("Error in neupd, info " + std::to_string(info));
if (ritz_option == arpack::which::largest_magnitude) {
for (int i = 0; i < nev; ++i) {
Real rval = std::real(d[i]);
Real rref = static_cast<Real>(N - (nev - 1) + i);
Real reps = std::fabs(rval - rref);
Real ival = std::imag(d[i]);
Real iref = -static_cast<Real>(N - (nev - 1) + i);
Real ieps = std::fabs(ival - iref);
std::cout << rval << " " << ival << " - " << rref << " " << iref << " - " << reps << " " << ieps << std::endl;
if (reps > tol_check || ieps > tol_check) throw std::domain_error("Correct eigenvalues not computed");
}
}
else if (ritz_option == arpack::which::largest_imaginary) {
for (int i = 0; i < nev; ++i) {
Real rval = std::real(d[i]);
Real rref = static_cast<Real>(nev - i);
Real reps = std::fabs(rval - rref);
Real ival = std::imag(d[i]);
Real iref = -static_cast<Real>(nev - i);
Real ieps = std::fabs(ival - iref);
std::cout << rval << " " << ival << " - " << rref << " " << iref << " - " << reps << " " << ieps << std::endl;
if (reps > tol_check || ieps > tol_check) throw std::domain_error("Correct eigenvalues not computed");
}
}
else {
throw std::domain_error("The input Ritz option is not allowed in this test file.");
}
std::cout << "------" << std::endl;
}
int main() {
sstats_c();
// arpack without debug
real_symmetric_runner<float>(1., arpack::which::largest_magnitude);
real_symmetric_runner<float>(1., arpack::which::largest_algebraic);
real_symmetric_runner<double>(1.e-05, arpack::which::largest_magnitude);
real_symmetric_runner<double>(1.e-05, arpack::which::largest_algebraic);
a_int nopx_c, nbx_c, nrorth_c, nitref_c, nrstrt_c;
float tsaupd_c, tsaup2_c, tsaitr_c, tseigt_c, tsgets_c, tsapps_c, tsconv_c;
float tnaupd_c, tnaup2_c, tnaitr_c, tneigt_c, tngets_c, tnapps_c, tnconv_c;
float tcaupd_c, tcaup2_c, tcaitr_c, tceigt_c, tcgets_c, tcapps_c, tcconv_c;
float tmvopx_c, tmvbx_c, tgetv0_c, titref_c, trvec_c;
stat_c(nopx_c, nbx_c, nrorth_c, nitref_c, nrstrt_c, tsaupd_c, tsaup2_c,
tsaitr_c, tseigt_c, tsgets_c, tsapps_c, tsconv_c, tnaupd_c, tnaup2_c,
tnaitr_c, tneigt_c, tngets_c, tnapps_c, tnconv_c, tcaupd_c, tcaup2_c,
tcaitr_c, tceigt_c, tcgets_c, tcapps_c, tcconv_c, tmvopx_c, tmvbx_c,
tgetv0_c, titref_c, trvec_c);
std::cout << "Timers : nopx " << nopx_c << ", tmvopx " << tmvopx_c;
std::cout << " - nbx " << nbx_c << ", tmvbx " << tmvbx_c << std::endl;
std::cout << "------" << std::endl;
// set debug flags
debug_c(6, -6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1);
// arpack with debug
complex_nonsymmetric_runner<float>(1., arpack::which::largest_magnitude);
complex_nonsymmetric_runner<float>(1., arpack::which::largest_imaginary);
complex_nonsymmetric_runner<double>(1.e-05, arpack::which::largest_magnitude);
complex_nonsymmetric_runner<double>(1.e-05, arpack::which::largest_imaginary);
return 0;
}
10.设置包含目录和库文件目录以及库文件名并打开MKL
11.完成计算,则表明已经完成了Arpack的编译。后面在自己的代码中,参考上述过程进行设置即可。
以上,即是Windows下采用Cmake构建编译大型开源稀疏矩阵特征值求解库Arpack-ng的过程。实践表明,这个库具有良好的性能,具体的使用,主要是dsaupd和dseupd这两个函数,但是具体参数很多,并且求解广义特征值时可能需要搭配一个求解线性方程组的求解库和高效的矩阵向量乘函数,这部分可以配合mkl的pardiso,mumps等高效的线性方程组求解库完成。
以上,即是本文的全部内容,感谢阅读。