1269 lines
54 KiB
C++
1269 lines
54 KiB
C++
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
|
|
|
|
#include <mkl.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <iomanip>
|
|
#include <limits>
|
|
#include <new>
|
|
#include <numeric>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "fesa/math/sparse_matrix.h"
|
|
#include "fesa/fem/dof_manager.h"
|
|
#include "fesa/math/vector.h"
|
|
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
|
#include "solvers/eigen/mkl_feast_eigen_solver_test_seam.h"
|
|
#include "math/dense_blas_internal.h"
|
|
|
|
namespace fesa {
|
|
|
|
namespace internal {
|
|
namespace {
|
|
constexpr std::size_t kMaximumCountCalls = 512U;
|
|
thread_local FeastBackendTestHook* g_feast_backend_test_hook = nullptr;
|
|
thread_local FeastWorkspaceTestHook* g_feast_workspace_test_hook = nullptr;
|
|
}
|
|
|
|
bool CanCallFeast(const std::size_t used) {
|
|
return used < kMaximumCountCalls;
|
|
}
|
|
|
|
bool CanStartUpperCandidate(const std::size_t used) {
|
|
return used <= 384U - 21U;
|
|
}
|
|
|
|
bool ProductFits(const std::size_t left, const std::size_t right) {
|
|
return left == 0U ||
|
|
right <= (std::numeric_limits<std::size_t>::max)() / left;
|
|
}
|
|
|
|
bool FitsDoubleStorage(const std::size_t count) {
|
|
return count <= std::vector<double>{}.max_size() &&
|
|
count <= static_cast<std::size_t>(
|
|
(std::numeric_limits<std::ptrdiff_t>::max)()) /
|
|
sizeof(double);
|
|
}
|
|
|
|
ScopedFeastBackendTestHook::ScopedFeastBackendTestHook(
|
|
FeastBackendTestHook hook)
|
|
: hook_(std::move(hook)), previous_(g_feast_backend_test_hook) {
|
|
g_feast_backend_test_hook = &hook_;
|
|
}
|
|
|
|
ScopedFeastBackendTestHook::~ScopedFeastBackendTestHook() {
|
|
g_feast_backend_test_hook = previous_;
|
|
}
|
|
|
|
ScopedFeastWorkspaceTestHook::ScopedFeastWorkspaceTestHook(
|
|
FeastWorkspaceTestHook hook)
|
|
: hook_(std::move(hook)), previous_(g_feast_workspace_test_hook) {
|
|
g_feast_workspace_test_hook = &hook_;
|
|
}
|
|
|
|
ScopedFeastWorkspaceTestHook::~ScopedFeastWorkspaceTestHook() {
|
|
g_feast_workspace_test_hook = previous_;
|
|
}
|
|
|
|
void ApplyFeastWorkspaceTestHook(const bool count_workspace) {
|
|
if (g_feast_workspace_test_hook != nullptr)
|
|
(*g_feast_workspace_test_hook)(count_workspace);
|
|
}
|
|
|
|
void ApplyFeastBackendTestHook(const FeastBackendTestCall& call, MKL_INT* m0,
|
|
MKL_INT* returned, MKL_INT* loop,
|
|
double* epsout, MKL_INT* info,
|
|
double* eigenvalues,
|
|
const std::size_t eigenvalue_count,
|
|
double* vectors,
|
|
const std::size_t vector_count,
|
|
double* residuals,
|
|
const std::size_t residual_count) {
|
|
if (g_feast_backend_test_hook == nullptr) return;
|
|
FeastBackendTestOutputs outputs{
|
|
static_cast<int>(*info), static_cast<std::ptrdiff_t>(*m0),
|
|
static_cast<std::ptrdiff_t>(*returned), static_cast<int>(*loop), *epsout,
|
|
std::vector<double>(eigenvalues, eigenvalues + eigenvalue_count),
|
|
std::vector<double>(vectors, vectors + vector_count),
|
|
std::vector<double>(residuals, residuals + residual_count)};
|
|
(*g_feast_backend_test_hook)(call, outputs);
|
|
*info = static_cast<MKL_INT>(outputs.info);
|
|
*m0 = static_cast<MKL_INT>(outputs.m0);
|
|
*returned = static_cast<MKL_INT>(outputs.returned);
|
|
*loop = static_cast<MKL_INT>(outputs.loop);
|
|
*epsout = outputs.epsout;
|
|
if (outputs.eigenvalues.size() == eigenvalue_count)
|
|
std::copy(outputs.eigenvalues.begin(), outputs.eigenvalues.end(), eigenvalues);
|
|
if (outputs.vectors.size() == vector_count)
|
|
std::copy(outputs.vectors.begin(), outputs.vectors.end(), vectors);
|
|
if (outputs.residuals.size() == residual_count)
|
|
std::copy(outputs.residuals.begin(), outputs.residuals.end(), residuals);
|
|
}
|
|
} // namespace internal
|
|
|
|
namespace {
|
|
|
|
constexpr std::uint64_t kSeed = 0x4D495443344D4645ULL;
|
|
|
|
Status Failure(const std::string& code, const std::string& identity,
|
|
const std::string& message) {
|
|
return Status::Failure(
|
|
FailureCategory::kSolver,
|
|
{{Severity::kError, code, {{}, 0U}, "FEAST", identity, message}});
|
|
}
|
|
|
|
bool FitsMkl(const std::size_t value) {
|
|
return value <=
|
|
static_cast<std::size_t>((std::numeric_limits<MKL_INT>::max)());
|
|
}
|
|
|
|
double NextUp(const double value) {
|
|
return std::nextafter(value, (std::numeric_limits<double>::infinity)());
|
|
}
|
|
|
|
double Delta(const double value, const double beta) {
|
|
const double up =
|
|
std::nextafter(value, (std::numeric_limits<double>::infinity)());
|
|
const double down =
|
|
std::nextafter(value, -(std::numeric_limits<double>::infinity)());
|
|
const double ulp = (std::max)(std::abs(up - value), std::abs(value - down));
|
|
return 1.0e-8 * beta + 64.0 * ulp;
|
|
}
|
|
|
|
/// @brief Restores the calling thread's oneMKL local-thread setting.
|
|
class LocalThreadGuard {
|
|
public:
|
|
LocalThreadGuard() : previous_(mkl_set_num_threads_local(1)) {}
|
|
~LocalThreadGuard() {
|
|
static_cast<void>(mkl_set_num_threads_local(previous_));
|
|
}
|
|
|
|
private:
|
|
int previous_;
|
|
};
|
|
|
|
struct Csr {
|
|
std::vector<MKL_INT> offsets;
|
|
std::vector<MKL_INT> columns;
|
|
std::vector<double> values;
|
|
};
|
|
|
|
Result<Csr> CopyUpperOneBased(const SparseMatrix& matrix, const double scale) {
|
|
Csr copy;
|
|
copy.offsets.reserve(matrix.Rows() + 1U);
|
|
copy.offsets.push_back(1);
|
|
const auto& offsets = matrix.RowOffsets();
|
|
const auto& columns = matrix.ColumnIndices();
|
|
const auto& values = matrix.Values();
|
|
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
|
|
for (std::size_t position = offsets[row]; position < offsets[row + 1U];
|
|
++position) {
|
|
const std::size_t column = columns[position];
|
|
if (column < row) {
|
|
continue;
|
|
}
|
|
if (!FitsMkl(column + 1U) || !FitsMkl(copy.values.size() + 1U)) {
|
|
return Result<Csr>::Failure(Failure(
|
|
"feast-integer-overflow", "csr-index",
|
|
"The public CSR index exceeds the oneMKL ILP64 integer range."));
|
|
}
|
|
copy.columns.push_back(static_cast<MKL_INT>(column + 1U));
|
|
copy.values.push_back(values[position] / scale);
|
|
}
|
|
if (!FitsMkl(copy.values.size() + 1U)) {
|
|
return Result<Csr>::Failure(Failure("feast-integer-overflow",
|
|
"csr-offset",
|
|
"The converted one-based CSR offset "
|
|
"exceeds the oneMKL integer range."));
|
|
}
|
|
copy.offsets.push_back(static_cast<MKL_INT>(copy.values.size() + 1U));
|
|
}
|
|
return Result<Csr>::Success(std::move(copy));
|
|
}
|
|
|
|
/// @brief Keeps the independently scaled full or upper-mirrored operator.
|
|
Result<SparseMatrix> ScaledActionMatrix(const SparseMatrix& matrix,
|
|
const double scale,
|
|
const bool mirror_upper = false) {
|
|
std::vector<CooContribution> contributions;
|
|
std::vector<std::vector<std::size_t>> columns(matrix.Rows());
|
|
for (std::size_t row = 0; row < matrix.Rows(); ++row) {
|
|
for (auto position = matrix.RowOffsets()[row];
|
|
position < matrix.RowOffsets()[row + 1U]; ++position) {
|
|
const auto column = matrix.ColumnIndices()[position];
|
|
if (mirror_upper && column < row) continue;
|
|
const auto value = matrix.Values()[position] / scale;
|
|
columns[row].push_back(column);
|
|
contributions.push_back({row, column, value, row, position});
|
|
if (mirror_upper && column != row) {
|
|
columns[column].push_back(row);
|
|
contributions.push_back({column, row, value, row, position});
|
|
}
|
|
}
|
|
}
|
|
SparsePattern pattern{{0U}, {}};
|
|
for (auto& row : columns) {
|
|
std::sort(row.begin(), row.end());
|
|
pattern.column_indices.insert(pattern.column_indices.end(), row.begin(),
|
|
row.end());
|
|
pattern.row_offsets.push_back(pattern.column_indices.size());
|
|
}
|
|
return SparseMatrix::FromCoo(matrix.Rows(), matrix.Columns(),
|
|
std::move(contributions), pattern);
|
|
}
|
|
|
|
Result<Csr> IdentityOneBased(const std::size_t size) {
|
|
if (!FitsMkl(size) || !FitsMkl(size + 1U)) {
|
|
return Result<Csr>::Failure(
|
|
Failure("feast-integer-overflow", "identity",
|
|
"The identity CSR exceeds oneMKL range."));
|
|
}
|
|
Csr identity;
|
|
identity.offsets.reserve(size + 1U);
|
|
identity.columns.reserve(size);
|
|
identity.values.assign(size, 1.0);
|
|
for (std::size_t index = 0U; index <= size; ++index) {
|
|
identity.offsets.push_back(static_cast<MKL_INT>(index + 1U));
|
|
if (index < size) {
|
|
identity.columns.push_back(static_cast<MKL_INT>(index + 1U));
|
|
}
|
|
}
|
|
return Result<Csr>::Success(std::move(identity));
|
|
}
|
|
|
|
Status ValidateSymmetry(const SparseMatrix& matrix) {
|
|
const auto& offsets = matrix.RowOffsets();
|
|
const auto& columns = matrix.ColumnIndices();
|
|
const auto& values = matrix.Values();
|
|
double scale = 0.0;
|
|
for (const double value : values) scale = (std::max)(scale, std::abs(value));
|
|
double norm = 0.0;
|
|
double asymmetry = 0.0;
|
|
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
|
|
for (std::size_t p = offsets[row]; p < offsets[row + 1U]; ++p) {
|
|
const std::size_t column = columns[p];
|
|
const auto begin =
|
|
columns.begin() + static_cast<std::ptrdiff_t>(offsets[column]);
|
|
const auto end =
|
|
columns.begin() + static_cast<std::ptrdiff_t>(offsets[column + 1U]);
|
|
const auto found = std::lower_bound(begin, end, row);
|
|
if (found == end || *found != row) {
|
|
return Failure("feast-nonsymmetric-csr",
|
|
std::to_string(row) + ":" + std::to_string(column),
|
|
"FEAST requires a full symmetric public CSR before "
|
|
"upper extraction.");
|
|
}
|
|
const std::size_t q = static_cast<std::size_t>(found - columns.begin());
|
|
if (scale != 0.0) {
|
|
norm = std::hypot(norm, values[p] / scale);
|
|
asymmetry =
|
|
std::hypot(asymmetry, values[p] / scale - values[q] / scale);
|
|
}
|
|
}
|
|
}
|
|
if (norm > 0.0 && asymmetry / norm > 1e-12)
|
|
return Failure("feast-nonsymmetric-csr", "whitened-operator",
|
|
"The public CSR violates the Frobenius symmetry contract.");
|
|
return Status::Ok();
|
|
}
|
|
|
|
void SetControls(std::array<MKL_INT, 128>& fpm, const bool count) {
|
|
feastinit(fpm.data());
|
|
fpm[0] = 0;
|
|
fpm[1] = 16;
|
|
fpm[2] = 12;
|
|
fpm[3] = 20;
|
|
fpm[4] = 1;
|
|
fpm[5] = 0; // Trace convergence alone can leave individual pairs inaccurate.
|
|
fpm[13] = 0;
|
|
fpm[26] = 1;
|
|
fpm[27] = 1;
|
|
fpm[49] = count ? 1 : 0;
|
|
fpm[63] = 0;
|
|
}
|
|
|
|
std::string FormatDouble(const double value) {
|
|
std::ostringstream stream;
|
|
stream << std::scientific << std::setprecision(17) << value;
|
|
return stream.str();
|
|
}
|
|
|
|
std::string CallHistory(const std::vector<FeastBackendCall>& calls) {
|
|
std::string history;
|
|
for (const FeastBackendCall& call : calls) {
|
|
if (!history.empty()) history += ";";
|
|
history += std::to_string(call.ordinal) + ":" + call.kind +
|
|
":info=" + std::to_string(call.info) +
|
|
":m0=" + std::to_string(call.allocated_m0) + ":(" +
|
|
FormatDouble(call.lower) + "," + FormatDouble(call.upper) + ")";
|
|
}
|
|
return history;
|
|
}
|
|
|
|
bool FillInitialBasis(std::vector<double>& vectors, const std::size_t n,
|
|
const std::size_t columns) {
|
|
std::fill(vectors.begin(), vectors.end(), 0.0);
|
|
if (columns == n) {
|
|
for (std::size_t i = 0U; i < n; ++i) vectors[i + i * n] = 1.0;
|
|
return true;
|
|
}
|
|
std::uint64_t state = kSeed;
|
|
for (double& value : vectors) {
|
|
state = 6364136223846793005ULL * state + 1442695040888963407ULL;
|
|
value = 2.0 * static_cast<double>(state >> 11U) * 0x1.0p-53 - 1.0;
|
|
}
|
|
std::vector<std::vector<double>> reflectors;
|
|
std::vector<double> signs;
|
|
reflectors.reserve(columns);
|
|
signs.reserve(columns);
|
|
for (std::size_t column = 0U; column < columns; ++column) {
|
|
double* current = vectors.data() + column * n;
|
|
double norm = 0.0;
|
|
double column_scale = 0.0;
|
|
for (std::size_t row = 0U; row < n; ++row) {
|
|
column_scale = std::hypot(column_scale, current[row]);
|
|
}
|
|
for (std::size_t row = column; row < n; ++row) {
|
|
norm = std::hypot(norm, current[row]);
|
|
}
|
|
if (!(column_scale > 0.0) || !(norm > 1.0e-12 * column_scale) ||
|
|
!std::isfinite(norm)) {
|
|
return false;
|
|
}
|
|
// Avoid cancellation in the reflector. Adjust Q's column sign below so
|
|
// R has a positive diagonal without subtracting nearly equal numbers.
|
|
const double alpha = -std::copysign(norm, current[column]);
|
|
signs.push_back(std::copysign(1.0, alpha));
|
|
std::vector<double> reflector(n - column, 0.0);
|
|
reflector[0U] = current[column] - alpha;
|
|
for (std::size_t row = column + 1U; row < n; ++row) {
|
|
reflector[row - column] = current[row];
|
|
}
|
|
double reflector_norm = 0.0;
|
|
for (const double value : reflector)
|
|
reflector_norm = std::hypot(reflector_norm, value);
|
|
if (!(reflector_norm > 0.0) || !std::isfinite(reflector_norm)) {
|
|
return false;
|
|
}
|
|
for (double& value : reflector) value /= reflector_norm;
|
|
for (std::size_t target = column; target < columns; ++target) {
|
|
const std::size_t length = n - column;
|
|
auto* target_column = vectors.data() + column + target * n;
|
|
const double dot = dense_blas_internal::DotValues(
|
|
reflector.data(), length, target_column);
|
|
dense_blas_internal::AxpyValues(-2.0 * dot, reflector.data(), length,
|
|
target_column);
|
|
}
|
|
reflectors.push_back(std::move(reflector));
|
|
}
|
|
std::fill(vectors.begin(), vectors.end(), 0.0);
|
|
for (std::size_t column = 0U; column < columns; ++column)
|
|
vectors[column + column * n] = 1.0;
|
|
for (std::size_t index = reflectors.size(); index > 0U; --index) {
|
|
const std::size_t column = index - 1U;
|
|
const auto& reflector = reflectors[column];
|
|
for (std::size_t target = 0U; target < columns; ++target) {
|
|
const std::size_t length = n - column;
|
|
auto* target_column = vectors.data() + column + target * n;
|
|
const double dot = dense_blas_internal::DotValues(
|
|
reflector.data(), length, target_column);
|
|
dense_blas_internal::AxpyValues(-2.0 * dot, reflector.data(), length,
|
|
target_column);
|
|
}
|
|
}
|
|
for (std::size_t column = 0U; column < columns; ++column)
|
|
for (std::size_t row = 0U; row < n; ++row)
|
|
vectors[row + column * n] *= signs[column];
|
|
double gram_error = 0.0;
|
|
for (std::size_t left = 0U; left < columns; ++left) {
|
|
for (std::size_t right = 0U; right < columns; ++right) {
|
|
const double dot = dense_blas_internal::DotValues(
|
|
vectors.data() + left * n, n, vectors.data() + right * n);
|
|
gram_error = std::hypot(gram_error, dot - (left == right ? 1.0 : 0.0));
|
|
}
|
|
}
|
|
return std::isfinite(gram_error) &&
|
|
gram_error / std::sqrt(static_cast<double>(columns)) <= 1e-12;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Result<FeastEigenResult> MklFeastEigenSolver::SolveGeneralized(
|
|
const SparseMatrix& stiffness, const SparseMatrix& mass,
|
|
const std::size_t requested_modes, const double mass_lower_bound,
|
|
const double classification_scale) const {
|
|
try {
|
|
return SolveImpl(stiffness, &mass, requested_modes, classification_scale,
|
|
mass_lower_bound);
|
|
} catch (const std::bad_alloc&) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
Failure("eigen-allocation-failed", "generalized-workspace",
|
|
"Allocation of generalized sparse-pencil storage failed."));
|
|
}
|
|
}
|
|
|
|
Result<FeastEigenResult> MklFeastEigenSolver::Solve(
|
|
const SparseMatrix& whitened_operator, const std::size_t requested_modes,
|
|
const double zero_operator_scale) const {
|
|
return SolveImpl(whitened_operator, nullptr, requested_modes,
|
|
zero_operator_scale, 1.0);
|
|
}
|
|
|
|
Result<FeastEigenResult> MklFeastEigenSolver::SolveImpl(
|
|
const SparseMatrix& whitened_operator, const SparseMatrix* mass,
|
|
const std::size_t requested_modes, const double zero_operator_scale,
|
|
const double mass_lower_bound) const try {
|
|
static_assert(sizeof(MKL_INT) == 8U,
|
|
"FEAST requires the installed ILP64 ABI.");
|
|
const Status csr_status = whitened_operator.Validate();
|
|
if (!csr_status.IsOk())
|
|
return Result<FeastEigenResult>::Failure(
|
|
Failure("feast-invalid-csr", "public-csr",
|
|
"The public sparse matrix failed CSR validation."));
|
|
const std::size_t n = whitened_operator.Rows();
|
|
if (mass != nullptr) {
|
|
if (!mass->Validate().IsOk() || mass->Rows() != n || mass->Columns() != n ||
|
|
!std::isfinite(mass_lower_bound) || !(mass_lower_bound > 0.0) ||
|
|
!std::isfinite(zero_operator_scale) || !(zero_operator_scale > 0.0)) {
|
|
return Result<FeastEigenResult>::Failure(Failure(
|
|
"feast-invalid-mass-bound", "generalized-pencil",
|
|
"A matching finite mass and certified positive bound are required."));
|
|
}
|
|
const Status mass_symmetry = ValidateSymmetry(*mass);
|
|
if (!mass_symmetry.IsOk())
|
|
return Result<FeastEigenResult>::Failure(mass_symmetry);
|
|
MklPardisoSolver spd_check;
|
|
const Status spd = spd_check.Factorize(*mass);
|
|
if (!spd.IsOk())
|
|
return Result<FeastEigenResult>::Failure(
|
|
Failure("feast-mass-not-positive-definite", "mass-spd",
|
|
"Sparse SPD mass factorization failed."));
|
|
}
|
|
if (n == 0U || whitened_operator.Columns() != n || requested_modes == 0U ||
|
|
requested_modes > n) {
|
|
return Result<FeastEigenResult>::Failure(Failure(
|
|
"feast-invalid-dimension", "matrix-or-request",
|
|
"FEAST requires 1 <= requested modes <= square matrix dimension."));
|
|
}
|
|
if (!FitsMkl(n) || !internal::ProductFits(n, n)) {
|
|
return Result<FeastEigenResult>::Failure(Failure(
|
|
"feast-integer-overflow", "matrix-shape",
|
|
"FEAST workspace dimensions exceed the private backend range."));
|
|
}
|
|
if (!internal::FitsDoubleStorage(n) || !internal::FitsDoubleStorage(n * n)) {
|
|
return Result<FeastEigenResult>::Failure(Failure(
|
|
"eigen-allocation-failed", "count-workspace",
|
|
"The required count workspace exceeds addressable double storage."));
|
|
}
|
|
const MKL_INT mkl_n = static_cast<MKL_INT>(n);
|
|
const Status symmetry = ValidateSymmetry(whitened_operator);
|
|
if (!symmetry.IsOk()) return Result<FeastEigenResult>::Failure(symmetry);
|
|
|
|
double infinity_norm = 0.0;
|
|
const auto& offsets = whitened_operator.RowOffsets();
|
|
const auto& values = whitened_operator.Values();
|
|
for (std::size_t row = 0U; row < n; ++row) {
|
|
double sum = 0.0;
|
|
for (std::size_t p = offsets[row]; p < offsets[row + 1U]; ++p) {
|
|
sum = mass != nullptr ? NextUp(sum + std::abs(values[p]))
|
|
: sum + std::abs(values[p]);
|
|
}
|
|
if (!std::isfinite(sum)) {
|
|
return Result<FeastEigenResult>::Failure(Failure(
|
|
"feast-nonfinite-operator-scale", "operator-scale",
|
|
"The operator infinity norm overflowed while computing its scale."));
|
|
}
|
|
infinity_norm = (std::max)(infinity_norm, sum);
|
|
}
|
|
const bool zero_operator = infinity_norm == 0.0;
|
|
if (zero_operator &&
|
|
(!(zero_operator_scale > 0.0) || !std::isfinite(zero_operator_scale))) {
|
|
return Result<FeastEigenResult>::Failure(Failure(
|
|
"feast-invalid-zero-scale", "zero-operator-scale",
|
|
"An exactly zero operator requires a finite positive physical scale."));
|
|
}
|
|
const double operator_scale =
|
|
mass != nullptr || zero_operator ? zero_operator_scale : infinity_norm;
|
|
if (!std::isfinite(operator_scale) || !(operator_scale > 0.0)) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
Failure("feast-nonfinite-operator-scale", "operator-scale",
|
|
"The operator infinity norm is not a finite positive scale."));
|
|
}
|
|
auto a = CopyUpperOneBased(whitened_operator, operator_scale);
|
|
if (!a.HasValue()) return Result<FeastEigenResult>::Failure(a.GetStatus());
|
|
auto b =
|
|
mass != nullptr ? CopyUpperOneBased(*mass, 1.0) : IdentityOneBased(n);
|
|
if (!b.HasValue()) return Result<FeastEigenResult>::Failure(b.GetStatus());
|
|
auto action_matrix = ScaledActionMatrix(whitened_operator, operator_scale);
|
|
if (!action_matrix.HasValue())
|
|
return Result<FeastEigenResult>::Failure(action_matrix.GetStatus());
|
|
|
|
double lower = -1.0;
|
|
double upper = 1.0;
|
|
double g_lower = 0.0;
|
|
double g_upper = 0.0;
|
|
double beta = 1.0;
|
|
if (mass != nullptr) {
|
|
// ||K||2 <= ||K||inf and M >= m_lower*I bound the genuine pencil.
|
|
const double bound =
|
|
NextUp(NextUp(infinity_norm / mass_lower_bound) / operator_scale);
|
|
g_lower = -bound;
|
|
g_upper = bound;
|
|
beta = (std::max)(1.0, bound);
|
|
lower = std::nextafter(g_lower - 16.0 * Delta(g_lower, beta),
|
|
-(std::numeric_limits<double>::infinity)());
|
|
upper = NextUp(g_upper + 16.0 * Delta(g_upper, beta));
|
|
} else if (!zero_operator) {
|
|
g_lower = (std::numeric_limits<double>::infinity)();
|
|
g_upper = -(std::numeric_limits<double>::infinity)();
|
|
const auto& columns = whitened_operator.ColumnIndices();
|
|
for (std::size_t row = 0U; row < n; ++row) {
|
|
double off_diagonal = 0.0;
|
|
double diagonal = 0.0;
|
|
for (std::size_t p = offsets[row]; p < offsets[row + 1U]; ++p) {
|
|
const double value = values[p] / operator_scale;
|
|
if (columns[p] == row)
|
|
diagonal = value;
|
|
else
|
|
off_diagonal = NextUp(off_diagonal + std::abs(value));
|
|
}
|
|
g_lower =
|
|
(std::min)(g_lower, std::nextafter(
|
|
diagonal - off_diagonal,
|
|
-(std::numeric_limits<double>::infinity)()));
|
|
g_upper = (std::max)(g_upper, NextUp(diagonal + off_diagonal));
|
|
beta = (std::max)(beta, NextUp(std::abs(diagonal) + off_diagonal));
|
|
}
|
|
const double lower_delta = Delta(g_lower, beta);
|
|
const double upper_delta = Delta(g_upper, beta);
|
|
lower = std::nextafter(g_lower - 16.0 * lower_delta,
|
|
-(std::numeric_limits<double>::infinity)());
|
|
upper = NextUp(g_upper + 16.0 * upper_delta);
|
|
}
|
|
if (!std::isfinite(lower) || !std::isfinite(upper) || !(lower < upper)) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
Failure("feast-invalid-enclosure", "gershgorin",
|
|
"A strict finite FEAST enclosure could not be constructed."));
|
|
}
|
|
|
|
LocalThreadGuard threads;
|
|
std::vector<FeastBackendCall> calls;
|
|
if (mass != nullptr)
|
|
calls.push_back({0U, "mass-spd", "original-mass-factorization", 0.0,
|
|
mass_lower_bound, 0U, 0, false, 0U, false, 0, false, 0.0,
|
|
"accepted",
|
|
"sparse-SPD; caller-certified-roundoff-bound"});
|
|
auto TraceFailure = [&](const std::string& code, const std::string& identity,
|
|
const std::string& message) {
|
|
return Failure(code, identity,
|
|
mass != nullptr
|
|
? message + " Backend calls=" + CallHistory(calls)
|
|
: message);
|
|
};
|
|
std::array<MKL_INT, 128> snapshot{};
|
|
SetControls(snapshot, false);
|
|
std::vector<std::int64_t> base_fpm;
|
|
base_fpm.reserve(snapshot.size());
|
|
for (const MKL_INT value : snapshot)
|
|
base_fpm.push_back(static_cast<std::int64_t>(value));
|
|
std::vector<double> count_e;
|
|
std::vector<double> count_res;
|
|
std::vector<double> count_x;
|
|
try {
|
|
internal::ApplyFeastWorkspaceTestHook(true);
|
|
count_e.assign(n, 0.0);
|
|
count_res.assign(n, 0.0);
|
|
count_x.assign(n * n, 0.0);
|
|
} catch (const std::bad_alloc&) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigen-allocation-failed", "count-workspace",
|
|
"Allocation of the required FEAST count workspace failed."));
|
|
}
|
|
|
|
auto Count = [&](const double call_lower, const double call_upper,
|
|
const std::string& purpose, std::size_t* count) -> Status {
|
|
if (!internal::CanCallFeast(calls.size()))
|
|
return TraceFailure("eigen-search-budget-exhausted", "count-calls",
|
|
"The bounded FEAST count-call budget was exhausted.");
|
|
std::array<MKL_INT, 128> fpm{};
|
|
SetControls(fpm, true);
|
|
MKL_INT m0 = static_cast<MKL_INT>(n);
|
|
MKL_INT returned = 0;
|
|
MKL_INT loop = 0;
|
|
MKL_INT info = 0;
|
|
double epsout = 0.0;
|
|
if (!FillInitialBasis(count_x, n, n)) {
|
|
return TraceFailure(
|
|
"eigenpair-validation-failed", "count-initial-basis",
|
|
"The prescribed count initial basis failed QR validation.");
|
|
}
|
|
const char uplo = 'U';
|
|
dfeast_scsrgv(&uplo, &mkl_n, a.Value().values.data(),
|
|
a.Value().offsets.data(), a.Value().columns.data(),
|
|
b.Value().values.data(), b.Value().offsets.data(),
|
|
b.Value().columns.data(), fpm.data(), &epsout, &loop,
|
|
&call_lower, &call_upper, &m0, count_e.data(), count_x.data(),
|
|
&returned, count_res.data(), &info);
|
|
internal::ApplyFeastBackendTestHook(
|
|
{true, calls.size(), call_lower, call_upper, n}, &m0, &returned,
|
|
&loop, &epsout, &info, count_e.data(), count_e.size(), count_x.data(),
|
|
count_x.size(), count_res.data(), count_res.size());
|
|
const bool count_available =
|
|
info == 0 && m0 >= 0 && static_cast<std::size_t>(m0) <= n;
|
|
FeastBackendCall event{calls.size(),
|
|
"count",
|
|
purpose,
|
|
call_lower,
|
|
call_upper,
|
|
n,
|
|
static_cast<int>(info),
|
|
count_available,
|
|
0U,
|
|
false,
|
|
0,
|
|
false,
|
|
0.0,
|
|
"",
|
|
""};
|
|
if (count_available) {
|
|
event.count = static_cast<std::size_t>(m0);
|
|
}
|
|
event.outcome = count_available ? "accepted" : "failed";
|
|
event.decision_reason =
|
|
count_available ? "accepted" : "vendor-info=" + std::to_string(info);
|
|
calls.push_back(std::move(event));
|
|
if (info != 0)
|
|
return TraceFailure(
|
|
"eigen-feast-failed", "info=" + std::to_string(info),
|
|
"FEAST did not certify the requested count interval.");
|
|
if (m0 < 0 || static_cast<std::size_t>(m0) > n) {
|
|
return TraceFailure("eigen-count-inconsistent", "count-output",
|
|
"FEAST count output is outside [0,n].");
|
|
}
|
|
*count = static_cast<std::size_t>(m0);
|
|
for (std::size_t i = 0U; i + 1U < calls.size(); ++i) {
|
|
const auto& prior = calls[i];
|
|
if (prior.kind != "count" || !prior.count_available ||
|
|
prior.lower != call_lower)
|
|
continue;
|
|
if ((prior.upper == call_upper && prior.count != *count) ||
|
|
(prior.upper < call_upper && prior.count > *count) ||
|
|
(prior.upper > call_upper && prior.count < *count))
|
|
return TraceFailure(
|
|
"eigen-count-inconsistent", "count-history",
|
|
"Successful prefix counts violate identity or monotonicity.");
|
|
}
|
|
return Status::Ok();
|
|
};
|
|
|
|
std::size_t count = 0U;
|
|
Status count_status = Status::Ok();
|
|
bool enclosure_certified = false;
|
|
for (std::size_t attempt = 0U; attempt < 3U; ++attempt) {
|
|
if (attempt > 0U) {
|
|
if (zero_operator) {
|
|
const double extent = std::ldexp(1.0, static_cast<int>(attempt));
|
|
lower = -extent;
|
|
upper = extent;
|
|
} else {
|
|
const double multiplier = std::ldexp(1.0, static_cast<int>(attempt));
|
|
lower =
|
|
std::nextafter(g_lower - 16.0 * multiplier * Delta(g_lower, beta),
|
|
-(std::numeric_limits<double>::infinity)());
|
|
upper = NextUp(g_upper + 16.0 * multiplier * Delta(g_upper, beta));
|
|
}
|
|
}
|
|
count_status = Count(lower, upper, "total-enclosure", &count);
|
|
if (count_status.IsOk() && count == n) {
|
|
enclosure_certified = true;
|
|
break;
|
|
}
|
|
const int info = calls.back().info;
|
|
const bool may_retry =
|
|
info == 1 || info == -5 || (count_status.IsOk() && count != n);
|
|
if (may_retry && attempt + 1U < 3U) {
|
|
calls.back().outcome = "retry";
|
|
calls.back().decision_reason =
|
|
count_status.IsOk() ? "count-mismatch"
|
|
: "retryable-vendor-info=" + std::to_string(info);
|
|
}
|
|
if (!may_retry) break;
|
|
}
|
|
if (!enclosure_certified) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
!count_status.IsOk()
|
|
? count_status
|
|
: TraceFailure(
|
|
"eigen-count-inconsistent", "total-enclosure",
|
|
"The enclosing FEAST count did not equal matrix dimension."));
|
|
}
|
|
if (mass != nullptr) {
|
|
// Count the actual pencil before removing any part of the conservative
|
|
// negative enclosure. A far-negative endpoint degrades contour filtering
|
|
// of low positive modes on a stiff mesh; an empty guarded prefix proves
|
|
// that moving it cannot remove a mode, including a repeated rigid cluster.
|
|
const double margin = Delta(0.0, beta);
|
|
const double candidate = -16.0 * margin;
|
|
const std::array<double, 3U> guards{candidate - margin, candidate,
|
|
candidate + margin};
|
|
if (lower < guards.front()) {
|
|
for (const double guard : guards) {
|
|
std::size_t negative_count = 0U;
|
|
const Status prefix_status =
|
|
Count(lower, guard, "lower-empty-prefix", &negative_count);
|
|
if (!prefix_status.IsOk())
|
|
return Result<FeastEigenResult>::Failure(prefix_status);
|
|
if (negative_count != 0U)
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"negative-eigenvalue-spectrum", "certified-negative-prefix",
|
|
"The generalized pencil contains significant negative "
|
|
"eigenvalues."));
|
|
}
|
|
lower = candidate;
|
|
calls.back().decision_reason = "guarded-empty-prefix; lowest-spectrum-preserved";
|
|
}
|
|
}
|
|
const double fixed_lower = lower;
|
|
const double enclosure_upper = upper;
|
|
double solve_upper = enclosure_upper;
|
|
std::string upper_search_termination =
|
|
n <= (std::min)(n, 2U * requested_modes + 8U) ? "count-target"
|
|
: "optimization-budget";
|
|
double search_lower = fixed_lower;
|
|
std::size_t certified_count = n;
|
|
std::vector<double> certified_uppers{enclosure_upper};
|
|
for (std::size_t midpoint_attempt = 0U;
|
|
upper_search_termination != "count-target" && midpoint_attempt < 32U;
|
|
++midpoint_attempt) {
|
|
if (!internal::CanStartUpperCandidate(calls.size())) {
|
|
upper_search_termination = "optimization-budget";
|
|
break;
|
|
}
|
|
const double midpoint = 0.5 * (search_lower + solve_upper);
|
|
if (!std::isfinite(midpoint) || midpoint == search_lower ||
|
|
midpoint == solve_upper) {
|
|
upper_search_termination = "no-midpoint";
|
|
break;
|
|
}
|
|
const double midpoint_delta = Delta(midpoint, beta);
|
|
const std::array<double, 7U> collision_offsets{0.0, 1.0, -1.0, 2.0,
|
|
-2.0, 4.0, -4.0};
|
|
bool certified = false;
|
|
bool optional_abandoned = false;
|
|
std::size_t candidate_count = 0U;
|
|
for (const double offset : collision_offsets) {
|
|
const double candidate = midpoint + offset * midpoint_delta;
|
|
const double margin = Delta(candidate, beta);
|
|
const std::array<double, 3U> guards{candidate - margin, candidate,
|
|
candidate + margin};
|
|
if (!(fixed_lower < guards[0U] && guards[2U] < enclosure_upper)) continue;
|
|
std::array<std::size_t, 3U> guard_counts{};
|
|
bool successful = true;
|
|
for (std::size_t guard = 0U; guard < guards.size(); ++guard) {
|
|
const Status guard_status = Count(fixed_lower, guards[guard],
|
|
"upper-guard", &guard_counts[guard]);
|
|
if (!guard_status.IsOk()) {
|
|
calls.back().outcome = "optional-abandoned";
|
|
const int optional_info = calls.back().info;
|
|
calls.back().decision_reason =
|
|
"optional-status=" + std::to_string(optional_info);
|
|
if (optional_info != 1 && optional_info != 2 && optional_info != 3 &&
|
|
optional_info != -5) {
|
|
return Result<FeastEigenResult>::Failure(guard_status);
|
|
}
|
|
optional_abandoned = true;
|
|
successful = false;
|
|
break;
|
|
}
|
|
}
|
|
if (optional_abandoned) break;
|
|
if (!successful || guard_counts[0U] != guard_counts[1U] ||
|
|
guard_counts[1U] != guard_counts[2U])
|
|
continue;
|
|
candidate_count = guard_counts[1U];
|
|
certified = true;
|
|
if (candidate_count >= requested_modes) {
|
|
solve_upper = candidate;
|
|
certified_uppers.push_back(candidate);
|
|
}
|
|
break;
|
|
}
|
|
if (optional_abandoned) {
|
|
upper_search_termination = "optional-status";
|
|
break;
|
|
}
|
|
if (!certified) {
|
|
upper_search_termination = "collision-exhausted";
|
|
break;
|
|
}
|
|
if (candidate_count >= requested_modes) {
|
|
certified_count = candidate_count;
|
|
if (candidate_count <= (std::min)(n, 2U * requested_modes + 8U)) {
|
|
upper_search_termination = "count-target";
|
|
break;
|
|
}
|
|
} else {
|
|
search_lower = midpoint;
|
|
}
|
|
if (solve_upper - search_lower <= 8.0 * Delta(midpoint, beta)) {
|
|
upper_search_termination = "width-limit";
|
|
break;
|
|
}
|
|
}
|
|
count = 0U;
|
|
count_status = Count(fixed_lower, solve_upper, "final-count", &count);
|
|
if (!count_status.IsOk() || count < requested_modes ||
|
|
count != certified_count) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigen-count-inconsistent", "final-count",
|
|
"The final FEAST interval count is not the certified guarded count."));
|
|
}
|
|
if (solve_upper != enclosure_upper) {
|
|
const double margin = Delta(solve_upper, beta);
|
|
std::size_t below = 0U;
|
|
std::size_t above = 0U;
|
|
const Status below_status =
|
|
Count(fixed_lower, solve_upper - margin, "final-count", &below);
|
|
const Status above_status =
|
|
Count(fixed_lower, solve_upper + margin, "final-count", &above);
|
|
if (!below_status.IsOk() || !above_status.IsOk() || below != count ||
|
|
above != count) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("eigen-count-inconsistent", "final-guard",
|
|
"The final interior endpoint is not guarded by equal "
|
|
"successful counts."));
|
|
}
|
|
}
|
|
if (count > (std::numeric_limits<std::size_t>::max)() - 8U ||
|
|
count > ((std::numeric_limits<std::size_t>::max)() - 1U) / 3U) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("eigen-allocation-failed", "solve-capacity",
|
|
"FEAST solve-capacity padding overflows size_t."));
|
|
}
|
|
const std::size_t padded_count =
|
|
(std::max)(count + 8U, (3U * count + 1U) / 2U);
|
|
std::size_t capacity = count == n ? n : (std::min)(n, padded_count);
|
|
if (!internal::ProductFits(n, capacity))
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("feast-workspace-overflow", "solve-workspace",
|
|
"The FEAST solve workspace size overflows."));
|
|
if (!internal::FitsDoubleStorage(n * capacity) ||
|
|
!internal::FitsDoubleStorage(capacity)) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigen-allocation-failed", "solve-workspace",
|
|
"The required solve workspace exceeds addressable double storage."));
|
|
}
|
|
std::vector<double> eigenvalues;
|
|
std::vector<double> residuals;
|
|
std::vector<double> vectors;
|
|
MKL_INT returned = 0;
|
|
MKL_INT info = 0;
|
|
bool solve_accepted = false;
|
|
std::size_t solve_capacity = capacity;
|
|
for (std::size_t attempt = 0U; attempt < 3U; ++attempt) {
|
|
if (!internal::CanCallFeast(calls.size()))
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigen-search-budget-exhausted", "solve-call",
|
|
"The total backend call budget is exhausted before solve."));
|
|
try {
|
|
internal::ApplyFeastWorkspaceTestHook(false);
|
|
eigenvalues.assign(solve_capacity, 0.0);
|
|
residuals.assign(solve_capacity, 0.0);
|
|
vectors.assign(n * solve_capacity, 0.0);
|
|
} catch (const std::bad_alloc&) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigen-allocation-failed", "solve-workspace",
|
|
"Allocation of the required FEAST solve workspace failed."));
|
|
}
|
|
if (!FillInitialBasis(vectors, n, solve_capacity)) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigenpair-validation-failed", "solve-initial-basis",
|
|
"The prescribed solve initial basis failed QR validation."));
|
|
}
|
|
std::array<MKL_INT, 128> fpm{};
|
|
SetControls(fpm, false);
|
|
MKL_INT m0 = static_cast<MKL_INT>(solve_capacity);
|
|
returned = 0;
|
|
MKL_INT loop = 0;
|
|
info = 0;
|
|
double epsout = 0.0;
|
|
const char uplo = 'U';
|
|
dfeast_scsrgv(&uplo, &mkl_n, a.Value().values.data(),
|
|
a.Value().offsets.data(), a.Value().columns.data(),
|
|
b.Value().values.data(), b.Value().offsets.data(),
|
|
b.Value().columns.data(), fpm.data(), &epsout, &loop,
|
|
&fixed_lower, &solve_upper, &m0, eigenvalues.data(),
|
|
vectors.data(), &returned, residuals.data(), &info);
|
|
internal::ApplyFeastBackendTestHook(
|
|
{false, calls.size(), fixed_lower, solve_upper, solve_capacity}, &m0,
|
|
&returned, &loop, &epsout, &info, eigenvalues.data(),
|
|
eigenvalues.size(), vectors.data(), vectors.size(), residuals.data(),
|
|
residuals.size());
|
|
const bool solve_outputs_available =
|
|
info == 0 && loop >= 0 && std::isfinite(epsout) && returned >= 0 &&
|
|
static_cast<std::size_t>(returned) <= solve_capacity;
|
|
calls.push_back(
|
|
{calls.size(), "solve", "solve", fixed_lower, solve_upper,
|
|
solve_capacity, static_cast<int>(info), solve_outputs_available,
|
|
solve_outputs_available ? static_cast<std::size_t>(returned) : 0U,
|
|
solve_outputs_available,
|
|
solve_outputs_available ? static_cast<int>(loop) : 0,
|
|
solve_outputs_available, solve_outputs_available ? epsout : 0.0,
|
|
solve_outputs_available ? "accepted" : "failed",
|
|
solve_outputs_available ? "accepted"
|
|
: "vendor-info=" + std::to_string(info)});
|
|
if (info == 0 && (!std::isfinite(epsout) || loop < 0)) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("invalid-eigenvalue-result", "solve-output",
|
|
"A successful FEAST solve returned nonfinite epsout or "
|
|
"invalid loop."));
|
|
}
|
|
if (info == 0 && returned >= 0 &&
|
|
static_cast<std::size_t>(returned) <= solve_capacity) {
|
|
for (std::size_t column = 0U; column < static_cast<std::size_t>(returned);
|
|
++column) {
|
|
if (!std::isfinite(eigenvalues[column]))
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"invalid-eigenvalue-result", std::to_string(column),
|
|
"A nonfinite eigenvalue cannot trigger an interval retry."));
|
|
for (std::size_t row = 0U; row < n; ++row)
|
|
if (!std::isfinite(vectors[row + column * n]))
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"invalid-eigenvalue-result", std::to_string(column),
|
|
"A nonfinite eigenvector cannot trigger an interval retry."));
|
|
}
|
|
}
|
|
bool endpoint_separated = info == 0 && returned >= 0 &&
|
|
static_cast<std::size_t>(returned) == count;
|
|
if (endpoint_separated) {
|
|
for (std::size_t index = 0U; index < count; ++index) {
|
|
if (!std::isfinite(eigenvalues[index])) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"invalid-eigenvalue-result", std::to_string(index),
|
|
"FEAST returned a nonfinite eigenvalue before endpoint "
|
|
"validation."));
|
|
}
|
|
endpoint_separated =
|
|
endpoint_separated &&
|
|
eigenvalues[index] - fixed_lower >= Delta(fixed_lower, beta) &&
|
|
solve_upper - eigenvalues[index] >= Delta(solve_upper, beta);
|
|
}
|
|
}
|
|
if (endpoint_separated) {
|
|
solve_accepted = true;
|
|
break;
|
|
}
|
|
const bool retryable =
|
|
!endpoint_separated &&
|
|
(info == 1 || info == 2 || info == 3 ||
|
|
(info == 0 &&
|
|
(returned < 0 || static_cast<std::size_t>(returned) != count ||
|
|
!endpoint_separated)));
|
|
if (!retryable || attempt == 2U) break;
|
|
calls.back().outcome = "retry";
|
|
calls.back().decision_reason =
|
|
info == 0 ? "endpoint-or-count-mismatch"
|
|
: "retryable-vendor-info=" + std::to_string(info);
|
|
double larger_upper = enclosure_upper;
|
|
for (const double certified : certified_uppers) {
|
|
if (certified > solve_upper && certified < larger_upper)
|
|
larger_upper = certified;
|
|
}
|
|
const bool interval_changed = larger_upper > solve_upper;
|
|
if (interval_changed) {
|
|
solve_upper = larger_upper;
|
|
std::size_t recounted = 0U;
|
|
const Status recount_status =
|
|
Count(fixed_lower, solve_upper, "final-count", &recounted);
|
|
if (!recount_status.IsOk() || recounted < requested_modes) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigen-count-inconsistent", "solve-retry",
|
|
"A larger certified solve interval could not be recounted."));
|
|
}
|
|
count = recounted;
|
|
if (solve_upper != enclosure_upper) {
|
|
const double retry_margin = Delta(solve_upper, beta);
|
|
std::size_t below = 0U;
|
|
std::size_t above = 0U;
|
|
const Status below_status = Count(
|
|
fixed_lower, solve_upper - retry_margin, "final-count", &below);
|
|
const Status above_status = Count(
|
|
fixed_lower, solve_upper + retry_margin, "final-count", &above);
|
|
if (!below_status.IsOk() || !above_status.IsOk() || below != count ||
|
|
above != count) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigen-count-inconsistent", "solve-retry-guard",
|
|
"The larger retry interval is not guarded by matching counts."));
|
|
}
|
|
}
|
|
if (count > (std::numeric_limits<std::size_t>::max)() - 8U ||
|
|
count > ((std::numeric_limits<std::size_t>::max)() - 1U) / 3U) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("eigen-allocation-failed", "solve-capacity",
|
|
"FEAST retry capacity padding overflows size_t."));
|
|
}
|
|
const std::size_t retry_padding =
|
|
(std::max)(count + 8U, (3U * count + 1U) / 2U);
|
|
capacity = count == n ? n : (std::min)(n, retry_padding);
|
|
}
|
|
const std::size_t previous_capacity = solve_capacity;
|
|
const std::size_t doubled =
|
|
solve_capacity > n / 2U ? n : 2U * solve_capacity;
|
|
solve_capacity = (std::max)(capacity, doubled);
|
|
if (solve_capacity == previous_capacity && !interval_changed) break;
|
|
}
|
|
if (!solve_accepted) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("eigen-feast-failed", "info=" + std::to_string(info),
|
|
"FEAST solve did not return the certified interval count "
|
|
"within its bounded retries."));
|
|
}
|
|
|
|
for (std::size_t index = 0U; index < count; ++index) {
|
|
if (!std::isfinite(eigenvalues[index])) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"invalid-eigenvalue-result", std::to_string(index),
|
|
"FEAST returned a nonfinite eigenvalue before sorting."));
|
|
}
|
|
for (std::size_t row = 0U; row < n; ++row) {
|
|
if (!std::isfinite(vectors[row + index * n])) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"invalid-eigenvalue-result", std::to_string(index),
|
|
"FEAST returned a nonfinite eigenvector before sorting."));
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::size_t> order(count);
|
|
std::iota(order.begin(), order.end(), 0U);
|
|
std::stable_sort(order.begin(), order.end(),
|
|
[&](const std::size_t left, const std::size_t right) {
|
|
return eigenvalues[left] < eigenvalues[right];
|
|
});
|
|
Matrix ordered_vectors(n, count);
|
|
std::vector<double> ordered_values;
|
|
std::vector<bool> ordered_backend_available;
|
|
std::vector<double> ordered_backend_res;
|
|
std::vector<double> ordered_mu;
|
|
std::vector<double> ordered_rho_b;
|
|
ordered_values.reserve(count);
|
|
ordered_backend_available.reserve(count);
|
|
ordered_backend_res.reserve(count);
|
|
ordered_mu.reserve(count);
|
|
ordered_rho_b.reserve(count);
|
|
for (std::size_t output = 0U; output < count; ++output) {
|
|
const std::size_t input = order[output];
|
|
const double mu = eigenvalues[input];
|
|
if (!std::isfinite(mu)) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("invalid-eigenvalue-result", std::to_string(input),
|
|
"FEAST returned a nonfinite eigenvalue."));
|
|
}
|
|
if (mu < -1.0e-9) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"negative-eigenvalue-spectrum", std::to_string(input),
|
|
"The raw normalized FEAST eigenvalue is significantly negative."));
|
|
}
|
|
if (mu - fixed_lower < Delta(fixed_lower, beta) ||
|
|
solve_upper - mu < Delta(solve_upper, beta)) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigen-endpoint-separation", std::to_string(input),
|
|
"A returned FEAST eigenvalue is too close to a solve endpoint."));
|
|
}
|
|
Vector raw(n);
|
|
for (std::size_t row = 0U; row < n; ++row)
|
|
raw[row] = vectors[row + input * n];
|
|
double norm = raw.Dot(raw);
|
|
if (mass != nullptr) {
|
|
norm = raw.Dot(mass->Multiply(raw));
|
|
}
|
|
norm = std::sqrt(norm);
|
|
if (!(norm > 0.0) || !std::isfinite(norm))
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("eigenpair-validation-failed", std::to_string(input),
|
|
"FEAST returned a non-normalizable eigenvector."));
|
|
double residual_norm = 0.0;
|
|
const auto action = action_matrix.Value().Multiply(raw);
|
|
const auto mass_action = mass != nullptr ? mass->Multiply(raw) : raw;
|
|
for (std::size_t row = 0U; row < n; ++row) {
|
|
const double difference = action[row] - mu * mass_action[row];
|
|
residual_norm += difference * difference;
|
|
ordered_vectors(row, output) = vectors[row + input * n] / norm;
|
|
if (!std::isfinite(ordered_vectors(row, output)))
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("eigenpair-validation-failed", std::to_string(input),
|
|
"FEAST returned a non-normalizable eigenvector."));
|
|
}
|
|
double residual_denominator = norm;
|
|
if (mass != nullptr) {
|
|
double norm_k = 0.0;
|
|
double norm_m = 0.0;
|
|
double euclidean = 0.0;
|
|
for (const double value : values)
|
|
norm_k = std::hypot(norm_k, value / operator_scale);
|
|
for (const double value : mass->Values())
|
|
norm_m = std::hypot(norm_m, value);
|
|
for (std::size_t row = 0U; row < n; ++row)
|
|
euclidean = std::hypot(euclidean, vectors[row + input * n]);
|
|
residual_denominator = (norm_k + std::abs(mu) * norm_m) * euclidean;
|
|
}
|
|
if (!std::isfinite(residual_norm) || !std::isfinite(residual_denominator))
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("eigenpair-validation-failed", std::to_string(input),
|
|
"A nonfinite independent residual numerator or scale "
|
|
"cannot certify a pair."));
|
|
const double rho_b =
|
|
residual_denominator == 0.0
|
|
? (residual_norm == 0.0 ? 0.0
|
|
: (std::numeric_limits<double>::infinity)())
|
|
: std::sqrt(residual_norm) / residual_denominator;
|
|
if (!std::isfinite(rho_b) || rho_b > 1.0e-10) {
|
|
if (mass != nullptr)
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigenpair-validation-failed", std::to_string(input),
|
|
"Independent original-pencil residual exceeds 1e-10: eta=" +
|
|
FormatDouble(rho_b) + ", mu=" + FormatDouble(mu) +
|
|
"; calls=" + CallHistory(calls)));
|
|
const FeastBackendCall& last_call = calls.back();
|
|
auto mirrored_matrix =
|
|
ScaledActionMatrix(whitened_operator, operator_scale, true);
|
|
if (!mirrored_matrix.HasValue())
|
|
return Result<FeastEigenResult>::Failure(mirrored_matrix.GetStatus());
|
|
const auto mirrored_action = mirrored_matrix.Value().Multiply(raw);
|
|
double mirrored_residual_squared = 0.0;
|
|
double action_difference_squared = 0.0;
|
|
for (std::size_t row = 0U; row < n; ++row) {
|
|
const double mirrored_residual =
|
|
mirrored_action[row] - mu * vectors[row + input * n];
|
|
mirrored_residual_squared += mirrored_residual * mirrored_residual;
|
|
const double action_difference =
|
|
action[row] - mirrored_action[row];
|
|
action_difference_squared += action_difference * action_difference;
|
|
}
|
|
const double rho_b_upper = std::sqrt(mirrored_residual_squared) / norm;
|
|
const double action_difference_norm =
|
|
std::sqrt(action_difference_squared);
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigenpair-validation-failed", std::to_string(input),
|
|
"Independent whitened residual exceeds 1e-10: rho_b=" +
|
|
FormatDouble(rho_b) + ", mu=" + FormatDouble(mu) +
|
|
", norm=" + FormatDouble(norm) +
|
|
", backend_res=" + FormatDouble(residuals[input]) +
|
|
", info=" + std::to_string(last_call.info) +
|
|
", epsout=" + FormatDouble(last_call.epsout) +
|
|
", loop=" + std::to_string(last_call.loop) +
|
|
", count=" + std::to_string(count) + ", m0=" +
|
|
std::to_string(last_call.allocated_m0) + ", interval=(" +
|
|
FormatDouble(fixed_lower) + "," + FormatDouble(solve_upper) +
|
|
"), calls=" + std::to_string(calls.size()) +
|
|
", history=" + CallHistory(calls) +
|
|
", rho_b_upper=" + FormatDouble(rho_b_upper) +
|
|
", full_upper_action_difference_norm=" +
|
|
FormatDouble(action_difference_norm) + "."));
|
|
}
|
|
const double lambda = mu * operator_scale;
|
|
if (!std::isfinite(lambda) || (mu != 0.0 && lambda == 0.0)) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"invalid-eigenvalue-result", std::to_string(input),
|
|
"Physical eigenvalue conversion overflowed or underflowed."));
|
|
}
|
|
ordered_values.push_back(lambda);
|
|
const bool backend_res_finite = std::isfinite(residuals[input]);
|
|
if (!backend_res_finite && mu != 0.0) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"invalid-eigenvalue-result", std::to_string(input),
|
|
"A nonzero FEAST mode has a nonfinite backend residual."));
|
|
}
|
|
ordered_backend_available.push_back(backend_res_finite);
|
|
ordered_backend_res.push_back(backend_res_finite ? residuals[input] : 0.0);
|
|
ordered_mu.push_back(mu);
|
|
ordered_rho_b.push_back(rho_b);
|
|
}
|
|
double gram_error = 0.0;
|
|
double gram_squared = 0.0;
|
|
Matrix mass_modes{n, count};
|
|
for (std::size_t right = 0; right < count; ++right) {
|
|
Vector mode(n);
|
|
for (std::size_t row = 0; row < n; ++row)
|
|
mode[row] = ordered_vectors(row, right);
|
|
const auto action = mass != nullptr ? mass->Multiply(mode) : std::move(mode);
|
|
for (std::size_t row = 0; row < n; ++row) {
|
|
if (!std::isfinite(action[row]))
|
|
return Result<FeastEigenResult>::Failure(
|
|
TraceFailure("eigenpair-validation-failed", "gram",
|
|
"The independent mass action is nonfinite."));
|
|
mass_modes(row, right) = action[row];
|
|
}
|
|
}
|
|
const auto gram = dense_blas_internal::MultiplyTransposeLeft(ordered_vectors,
|
|
mass_modes);
|
|
for (std::size_t left = 0U; left < count; ++left) {
|
|
for (std::size_t right = 0U; right < count; ++right) {
|
|
const double dot = gram(left, right);
|
|
const double expected = left == right ? 1.0 : 0.0;
|
|
const double difference = std::abs(dot - expected);
|
|
gram_squared += difference * difference;
|
|
gram_error = (std::max)(gram_error, difference);
|
|
if (std::abs(dot - expected) > 1.0e-10)
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigenpair-validation-failed",
|
|
std::to_string(left) + ":" + std::to_string(right),
|
|
"The all-c FEAST vectors fail the independent Gram check."));
|
|
}
|
|
}
|
|
gram_error = std::sqrt(gram_squared /
|
|
(mass != nullptr ? 1.0 : static_cast<double>(count)));
|
|
if (!std::isfinite(gram_error) || gram_error > 1.0e-10) {
|
|
return Result<FeastEigenResult>::Failure(TraceFailure(
|
|
"eigenpair-validation-failed", "gram",
|
|
"The normalized all-c Frobenius Gram error exceeds 1e-10: " +
|
|
FormatDouble(gram_error) + "."));
|
|
}
|
|
char version[256]{};
|
|
mkl_get_version_string(version, static_cast<int>(sizeof(version)));
|
|
FeastEigenResult result{requested_modes,
|
|
std::move(ordered_values),
|
|
std::move(ordered_vectors),
|
|
operator_scale,
|
|
order,
|
|
std::move(ordered_backend_available),
|
|
std::move(ordered_backend_res),
|
|
std::move(ordered_mu),
|
|
std::move(ordered_rho_b),
|
|
gram_error,
|
|
std::move(calls),
|
|
std::move(base_fpm),
|
|
version,
|
|
"ILP64",
|
|
1U,
|
|
kSeed,
|
|
fixed_lower,
|
|
enclosure_upper,
|
|
fixed_lower,
|
|
solve_upper,
|
|
upper_search_termination};
|
|
return Result<FeastEigenResult>::Success(std::move(result));
|
|
} catch (const std::invalid_argument&) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
Failure("eigenpair-validation-failed", "independent-action",
|
|
"Modal product inputs must be finite and shape compatible."));
|
|
} catch (const std::length_error&) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
Failure("feast-integer-overflow", "independent-action",
|
|
"Independent sparse action exceeds the backend range."));
|
|
} catch (const std::runtime_error&) {
|
|
return Result<FeastEigenResult>::Failure(
|
|
Failure("eigen-multiplication-failed", "independent-action",
|
|
"Independent sparse action failed."));
|
|
}
|
|
|
|
} // namespace fesa
|