632 lines
25 KiB
C++
632 lines
25 KiB
C++
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <mkl.h>
|
|
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <limits>
|
|
#include <new>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "fesa/fem/dof_manager.h"
|
|
#include "fesa/math/sparse_matrix.h"
|
|
#include "solvers/eigen/mkl_feast_eigen_solver_test_seam.h"
|
|
|
|
namespace {
|
|
|
|
fesa::SparseMatrix MakeDenseCsr(const std::size_t size,
|
|
const std::vector<double>& values) {
|
|
EXPECT_EQ(values.size(), size * size);
|
|
fesa::SparsePattern pattern;
|
|
std::vector<fesa::CooContribution> contributions;
|
|
pattern.row_offsets.push_back(0U);
|
|
for (std::size_t row = 0U; row < size; ++row) {
|
|
for (std::size_t column = 0U; column < size; ++column) {
|
|
pattern.column_indices.push_back(column);
|
|
contributions.push_back(
|
|
{row, column, values[row * size + column], row, column});
|
|
}
|
|
pattern.row_offsets.push_back(pattern.column_indices.size());
|
|
}
|
|
auto matrix = fesa::SparseMatrix::FromCoo(size, size,
|
|
std::move(contributions), pattern);
|
|
EXPECT_TRUE(matrix.HasValue());
|
|
return std::move(matrix.Value());
|
|
}
|
|
|
|
void ExpectUnitGram(const fesa::Matrix& vectors) {
|
|
for (std::size_t left = 0U; left < vectors.Columns(); ++left) {
|
|
for (std::size_t right = 0U; right < vectors.Columns(); ++right) {
|
|
double dot = 0.0;
|
|
for (std::size_t row = 0U; row < vectors.Rows(); ++row) {
|
|
dot += vectors(row, left) * vectors(row, right);
|
|
}
|
|
EXPECT_NEAR(dot, left == right ? 1.0 : 0.0, 1.0e-10);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RequireSuccess(const fesa::Result<fesa::FeastEigenResult>& result) {
|
|
if (result.HasValue()) return;
|
|
const auto& diagnostics = result.GetStatus().Diagnostics();
|
|
ASSERT_FALSE(diagnostics.empty());
|
|
ADD_FAILURE() << diagnostics.front().code << " "
|
|
<< diagnostics.front().entity_identity << " "
|
|
<< diagnostics.front().message;
|
|
}
|
|
|
|
void ExpectFailureCode(const fesa::Result<fesa::FeastEigenResult>& result,
|
|
const char* code) {
|
|
ASSERT_FALSE(result.HasValue());
|
|
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
|
EXPECT_EQ(result.GetStatus().Diagnostics().front().code, code);
|
|
}
|
|
|
|
void ExpectSameBackendCalls(const std::vector<fesa::FeastBackendCall>& first,
|
|
const std::vector<fesa::FeastBackendCall>& second) {
|
|
ASSERT_EQ(first.size(), second.size());
|
|
for (std::size_t index = 0U; index < first.size(); ++index) {
|
|
EXPECT_EQ(first[index].ordinal, second[index].ordinal);
|
|
EXPECT_EQ(first[index].kind, second[index].kind);
|
|
EXPECT_EQ(first[index].purpose, second[index].purpose);
|
|
EXPECT_DOUBLE_EQ(first[index].lower, second[index].lower);
|
|
EXPECT_DOUBLE_EQ(first[index].upper, second[index].upper);
|
|
EXPECT_EQ(first[index].allocated_m0, second[index].allocated_m0);
|
|
EXPECT_EQ(first[index].info, second[index].info);
|
|
EXPECT_EQ(first[index].count_available, second[index].count_available);
|
|
EXPECT_EQ(first[index].count, second[index].count);
|
|
EXPECT_EQ(first[index].outcome, second[index].outcome);
|
|
EXPECT_EQ(first[index].decision_reason, second[index].decision_reason);
|
|
}
|
|
}
|
|
|
|
void ExpectSameProjector(const fesa::Matrix& first, const fesa::Matrix& second,
|
|
const std::size_t columns) {
|
|
ASSERT_EQ(first.Rows(), second.Rows());
|
|
double squared_difference = 0.0;
|
|
for (std::size_t row = 0U; row < first.Rows(); ++row) {
|
|
for (std::size_t column = 0U; column < first.Rows(); ++column) {
|
|
double first_value = 0.0;
|
|
double second_value = 0.0;
|
|
for (std::size_t mode = 0U; mode < columns; ++mode) {
|
|
first_value += first(row, mode) * first(column, mode);
|
|
second_value += second(row, mode) * second(column, mode);
|
|
}
|
|
const double difference = first_value - second_value;
|
|
squared_difference += difference * difference;
|
|
}
|
|
}
|
|
EXPECT_LE(std::sqrt(squared_difference /
|
|
(2.0 * static_cast<double>(columns))),
|
|
1.0e-6);
|
|
}
|
|
|
|
void ExpectThreadSettingRestored(
|
|
const fesa::Result<fesa::FeastEigenResult>& result, const char* code) {
|
|
ExpectFailureCode(result, code);
|
|
const int restored = mkl_set_num_threads_local(2);
|
|
EXPECT_EQ(restored, 2);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(MklFeastEigenSolver, SolvesN1AndRetainsBackendEvidence) {
|
|
const auto matrix = MakeDenseCsr(1U, {4.0});
|
|
fesa::MklFeastEigenSolver solver;
|
|
|
|
const auto result = solver.Solve(matrix, 1U, 1.0);
|
|
|
|
RequireSuccess(result);
|
|
if (!result.HasValue()) return;
|
|
EXPECT_EQ(result.Value().requested_modes, 1U);
|
|
ASSERT_EQ(result.Value().eigenvalues.size(), 1U);
|
|
EXPECT_NEAR(result.Value().eigenvalues[0U], 4.0, 1.0e-10);
|
|
EXPECT_DOUBLE_EQ(result.Value().operator_scale, 4.0);
|
|
EXPECT_EQ(result.Value().eigenvectors.Rows(), 1U);
|
|
EXPECT_EQ(result.Value().eigenvectors.Columns(), 1U);
|
|
EXPECT_EQ(result.Value().thread_count, 1U);
|
|
ASSERT_EQ(result.Value().base_fpm.size(), 128U);
|
|
EXPECT_EQ(result.Value().base_fpm[5U], 0);
|
|
ASSERT_FALSE(result.Value().backend_calls.empty());
|
|
EXPECT_EQ(result.Value().backend_calls.front().purpose, "total-enclosure");
|
|
EXPECT_EQ(result.Value().backend_calls.back().kind, "solve");
|
|
EXPECT_TRUE(result.Value().backend_calls.back().count_available);
|
|
EXPECT_EQ(result.Value().backend_calls.back().count,
|
|
result.Value().eigenvalues.size());
|
|
EXPECT_TRUE(result.Value().backend_calls.back().loop_available);
|
|
EXPECT_TRUE(result.Value().backend_calls.back().epsout_available);
|
|
EXPECT_EQ(result.Value().backend_calls.back().outcome, "accepted");
|
|
EXPECT_EQ(result.Value().backend_calls.back().decision_reason, "accepted");
|
|
EXPECT_FALSE(result.Value().upper_search_termination.empty());
|
|
ExpectUnitGram(result.Value().eigenvectors);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, SolvesAllZeroWithExplicitPhysicalScale) {
|
|
const auto matrix = MakeDenseCsr(2U, {0.0, 0.0, 0.0, 0.0});
|
|
fesa::MklFeastEigenSolver solver;
|
|
|
|
const auto result = solver.Solve(matrix, 2U, 7.0);
|
|
|
|
RequireSuccess(result);
|
|
if (!result.HasValue()) return;
|
|
ASSERT_EQ(result.Value().eigenvalues.size(), 2U);
|
|
EXPECT_DOUBLE_EQ(result.Value().operator_scale, 7.0);
|
|
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[0U], 0.0);
|
|
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[1U], 0.0);
|
|
ExpectUnitGram(result.Value().eigenvectors);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, PreservesRepeatedMultiplicityAndStableOrder) {
|
|
const auto matrix =
|
|
MakeDenseCsr(3U, {2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 5.0});
|
|
fesa::MklFeastEigenSolver solver;
|
|
|
|
const auto first = solver.Solve(matrix, 3U, 1.0);
|
|
const auto second = solver.Solve(matrix, 3U, 1.0);
|
|
const auto third = solver.Solve(matrix, 3U, 1.0);
|
|
|
|
RequireSuccess(first);
|
|
RequireSuccess(second);
|
|
RequireSuccess(third);
|
|
if (!first.HasValue() || !second.HasValue() || !third.HasValue()) return;
|
|
ASSERT_EQ(first.Value().eigenvalues.size(), 3U);
|
|
EXPECT_NEAR(first.Value().eigenvalues[0U], 2.0, 1.0e-10);
|
|
EXPECT_NEAR(first.Value().eigenvalues[1U], 2.0, 1.0e-10);
|
|
EXPECT_NEAR(first.Value().eigenvalues[2U], 5.0, 1.0e-10);
|
|
EXPECT_EQ(first.Value().column_ids, second.Value().column_ids);
|
|
EXPECT_EQ(first.Value().column_ids, third.Value().column_ids);
|
|
EXPECT_EQ(first.Value().eigenvalues, second.Value().eigenvalues);
|
|
EXPECT_EQ(first.Value().eigenvalues, third.Value().eigenvalues);
|
|
ExpectSameBackendCalls(first.Value().backend_calls,
|
|
second.Value().backend_calls);
|
|
ExpectSameBackendCalls(first.Value().backend_calls,
|
|
third.Value().backend_calls);
|
|
ExpectSameProjector(first.Value().eigenvectors, second.Value().eigenvectors,
|
|
2U);
|
|
ExpectSameProjector(first.Value().eigenvectors, third.Value().eigenvectors,
|
|
2U);
|
|
ExpectUnitGram(first.Value().eigenvectors);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, UsesGuardedPrefixWithoutDroppingRepeatedCluster) {
|
|
constexpr std::size_t kSize = 64U;
|
|
std::vector<double> values(kSize * kSize, 0.0);
|
|
for (std::size_t index = 0U; index < kSize; ++index) {
|
|
values[index * kSize + index] = index < 40U ? 1.0 : index - 38U;
|
|
}
|
|
const auto matrix = MakeDenseCsr(kSize, values);
|
|
fesa::MklFeastEigenSolver solver;
|
|
|
|
const auto one_mode = solver.Solve(matrix, 1U, 1.0);
|
|
const auto forty_first = solver.Solve(matrix, 41U, 1.0);
|
|
|
|
RequireSuccess(one_mode);
|
|
RequireSuccess(forty_first);
|
|
if (!one_mode.HasValue() || !forty_first.HasValue()) return;
|
|
EXPECT_GE(one_mode.Value().eigenvalues.size(), 40U);
|
|
EXPECT_GE(forty_first.Value().eigenvalues.size(), 41U);
|
|
for (std::size_t index = 0U; index < 40U; ++index) {
|
|
EXPECT_NEAR(one_mode.Value().eigenvalues[index], 1.0, 1.0e-10);
|
|
}
|
|
EXPECT_NEAR(forty_first.Value().eigenvalues[40U], 2.0, 1.0e-10);
|
|
EXPECT_EQ(one_mode.Value().backend_calls.back().kind, "solve");
|
|
EXPECT_EQ(forty_first.Value().backend_calls.back().kind, "solve");
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsSignificantNegativeSpectrum) {
|
|
const auto matrix = MakeDenseCsr(1U, {-2.0});
|
|
fesa::MklFeastEigenSolver solver;
|
|
|
|
const auto result = solver.Solve(matrix, 1U, 1.0);
|
|
|
|
ASSERT_FALSE(result.HasValue());
|
|
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kSolver);
|
|
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U);
|
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
|
|
"negative-eigenvalue-spectrum");
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, KeepsCloseDistinctModesSeparated) {
|
|
const auto matrix = MakeDenseCsr(2U, {1.0, 0.0, 0.0, 1.001});
|
|
fesa::MklFeastEigenSolver solver;
|
|
|
|
const auto result = solver.Solve(matrix, 2U, 1.0);
|
|
|
|
RequireSuccess(result);
|
|
if (!result.HasValue()) return;
|
|
ASSERT_EQ(result.Value().eigenvalues.size(), 2U);
|
|
EXPECT_NEAR(result.Value().eigenvalues[0U], 1.0, 1.0e-10);
|
|
EXPECT_NEAR(result.Value().eigenvalues[1U], 1.001, 1.0e-10);
|
|
EXPECT_NEAR(std::abs(result.Value().eigenvectors(0U, 0U)), 1.0, 1.0e-10);
|
|
EXPECT_NEAR(std::abs(result.Value().eigenvectors(1U, 1U)), 1.0, 1.0e-10);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RestoresCallingThreadLocalSetting) {
|
|
const auto matrix = MakeDenseCsr(1U, {4.0});
|
|
const int prior = mkl_set_num_threads_local(2);
|
|
fesa::MklFeastEigenSolver solver;
|
|
|
|
const auto result = solver.Solve(matrix, 1U, 1.0);
|
|
const int restored = mkl_set_num_threads_local(2);
|
|
static_cast<void>(mkl_set_num_threads_local(prior));
|
|
|
|
RequireSuccess(result);
|
|
if (!result.HasValue()) return;
|
|
EXPECT_EQ(restored, 2);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsInvalidRequestAndZeroScale) {
|
|
const auto matrix = MakeDenseCsr(1U, {0.0});
|
|
fesa::MklFeastEigenSolver solver;
|
|
|
|
const auto zero_request = solver.Solve(matrix, 0U, 1.0);
|
|
ASSERT_FALSE(zero_request.HasValue());
|
|
EXPECT_EQ(zero_request.GetStatus().Diagnostics()[0U].code,
|
|
"feast-invalid-dimension");
|
|
const auto invalid_scale = solver.Solve(matrix, 1U, 0.0);
|
|
ASSERT_FALSE(invalid_scale.HasValue());
|
|
EXPECT_EQ(invalid_scale.GetStatus().Diagnostics()[0U].code,
|
|
"feast-invalid-zero-scale");
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, PreservesLowestPrefixForZeroAndPositiveSpectra) {
|
|
const std::vector<std::vector<double>> spectra{{2.0},
|
|
{0.0, 0.0, 0.0, 2.0, 5.0, 9.0},
|
|
{1.0, 2.0, 4.0, 8.0},
|
|
{0.0, 0.0, 0.0, 0.0}};
|
|
const std::vector<std::vector<std::size_t>> requests{
|
|
{1U}, {1U, 4U, 6U}, {1U, 2U, 4U}, {1U, 4U}};
|
|
const fesa::MklFeastEigenSolver solver;
|
|
for (std::size_t case_index = 0U; case_index < spectra.size(); ++case_index) {
|
|
const auto& spectrum = spectra[case_index];
|
|
const auto n = spectrum.size();
|
|
std::vector<double> values(n * n, 0.0);
|
|
for (std::size_t i = 0U; i < n; ++i) values[i * n + i] = spectrum[i];
|
|
const auto matrix = MakeDenseCsr(n, values);
|
|
for (const auto requested : requests[case_index]) {
|
|
const auto result = solver.Solve(matrix, requested, 1.0);
|
|
RequireSuccess(result);
|
|
ASSERT_TRUE(result.HasValue());
|
|
ASSERT_GE(result.Value().eigenvalues.size(), requested);
|
|
for (std::size_t i = 0U; i < requested; ++i)
|
|
EXPECT_NEAR(result.Value().eigenvalues[i], spectrum[i], 1e-10);
|
|
ExpectUnitGram(result.Value().eigenvectors);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, ResolvesCloseDistinctSpectrumAfterGivensCongruence) {
|
|
const std::array<double, 4> spectrum{1.0, 1.0 + std::ldexp(1.0, -28),
|
|
1.0 + std::ldexp(1.0, -27), 8.0};
|
|
const std::array<std::array<double, 4>, 4> q{{{{0.6, 0.0, 0.0, -0.8}},
|
|
{{0.0, 0.6, -0.8, 0.0}},
|
|
{{0.0, 0.8, 0.6, 0.0}},
|
|
{{0.8, 0.0, 0.0, 0.6}}}};
|
|
std::vector<double> values(16U, 0.0);
|
|
for (std::size_t i = 0U; i < 4U; ++i)
|
|
for (std::size_t j = 0U; j < 4U; ++j)
|
|
for (std::size_t k = 0U; k < 4U; ++k)
|
|
values[4U * i + j] += q[i][k] * spectrum[k] * q[j][k];
|
|
const auto result =
|
|
fesa::MklFeastEigenSolver{}.Solve(MakeDenseCsr(4U, values), 2U, 1.0);
|
|
RequireSuccess(result);
|
|
ASSERT_TRUE(result.HasValue());
|
|
ASSERT_EQ(result.Value().eigenvalues.size(), 4U);
|
|
for (std::size_t i = 0U; i < 4U; ++i)
|
|
EXPECT_NEAR(result.Value().eigenvalues[i], spectrum[i], 1e-12);
|
|
EXPECT_GT(result.Value().eigenvalues[1U] - result.Value().eigenvalues[0U],
|
|
1e-9);
|
|
ExpectUnitGram(result.Value().eigenvectors);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsFatalCountStatusWithoutRetry) {
|
|
std::size_t hook_calls = 0U;
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[&](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
++hook_calls;
|
|
if (call.count && call.ordinal == 0U) outputs.info = -1;
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(1U, {2.0}), 1U, 1.0);
|
|
|
|
ExpectFailureCode(result, "eigen-feast-failed");
|
|
EXPECT_EQ(hook_calls, 1U);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, BoundsCountCallsAndUpperSearchReservations) {
|
|
constexpr std::size_t kMaximum = (std::numeric_limits<std::size_t>::max)();
|
|
EXPECT_TRUE(fesa::internal::CanCallFeast(511U));
|
|
EXPECT_FALSE(fesa::internal::CanCallFeast(512U));
|
|
EXPECT_FALSE(fesa::internal::CanCallFeast(kMaximum));
|
|
EXPECT_TRUE(fesa::internal::CanStartUpperCandidate(363U));
|
|
EXPECT_FALSE(fesa::internal::CanStartUpperCandidate(364U));
|
|
EXPECT_FALSE(fesa::internal::CanStartUpperCandidate(kMaximum));
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, BoundsPrivateWorkspaceProductsBeforeAllocation) {
|
|
constexpr std::size_t kMaximum = (std::numeric_limits<std::size_t>::max)();
|
|
EXPECT_TRUE(fesa::internal::ProductFits(0U, kMaximum));
|
|
EXPECT_TRUE(fesa::internal::ProductFits(1U, kMaximum));
|
|
EXPECT_FALSE(fesa::internal::ProductFits(kMaximum, 2U));
|
|
|
|
const std::size_t byte_limit = static_cast<std::size_t>(
|
|
(std::numeric_limits<std::ptrdiff_t>::max)() / sizeof(double));
|
|
const std::size_t storage_limit =
|
|
(std::min)(byte_limit, std::vector<double>{}.max_size());
|
|
EXPECT_TRUE(fesa::internal::FitsDoubleStorage(0U));
|
|
EXPECT_TRUE(fesa::internal::FitsDoubleStorage(storage_limit));
|
|
EXPECT_FALSE(fesa::internal::FitsDoubleStorage(kMaximum));
|
|
if (storage_limit < kMaximum)
|
|
EXPECT_FALSE(fesa::internal::FitsDoubleStorage(storage_limit + 1U));
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, TranslatesCountWorkspaceAllocationFailure) {
|
|
const int prior = mkl_set_num_threads_local(2);
|
|
fesa::internal::ScopedFeastWorkspaceTestHook hook([](const bool count) {
|
|
if (count) throw std::bad_alloc();
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(1U, {2.0}), 1U, 1.0);
|
|
|
|
ExpectThreadSettingRestored(result, "eigen-allocation-failed");
|
|
static_cast<void>(mkl_set_num_threads_local(prior));
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, TranslatesSolveWorkspaceAllocationFailure) {
|
|
const int prior = mkl_set_num_threads_local(2);
|
|
fesa::internal::ScopedFeastWorkspaceTestHook hook([](const bool count) {
|
|
if (!count) throw std::bad_alloc();
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(1U, {2.0}), 1U, 1.0);
|
|
|
|
ExpectThreadSettingRestored(result, "eigen-allocation-failed");
|
|
static_cast<void>(mkl_set_num_threads_local(prior));
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, StopsInitialInfoOneAfterThreeCountCalls) {
|
|
std::size_t count_calls = 0U;
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[&](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (call.count) {
|
|
++count_calls;
|
|
outputs.info = 1;
|
|
}
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(1U, {2.0}), 1U, 1.0);
|
|
|
|
ExpectFailureCode(result, "eigen-feast-failed");
|
|
EXPECT_EQ(count_calls, 3U);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsFinalCountHistoryMismatch) {
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
// n=1 has total enclosure at ordinal 0 and final count at ordinal 1.
|
|
if (call.count && call.ordinal == 1U) outputs.m0 = 0;
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(1U, {2.0}), 1U, 1.0);
|
|
|
|
ExpectFailureCode(result, "eigen-count-inconsistent");
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsDecreasingCountAcrossDistinctUpperGuards) {
|
|
constexpr std::size_t kSize = 11U;
|
|
std::vector<double> values(kSize * kSize, 0.0);
|
|
for (std::size_t index = 0U; index < kSize; ++index)
|
|
values[index * kSize + index] = static_cast<double>(index + 1U);
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[kSize](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
// Ordinals 1 and 2 are the first candidate's increasing upper guards.
|
|
if (call.count && call.ordinal == 1U) outputs.m0 = kSize;
|
|
if (call.count && call.ordinal == 2U) outputs.m0 = kSize - 1U;
|
|
});
|
|
|
|
const auto result =
|
|
fesa::MklFeastEigenSolver{}.Solve(MakeDenseCsr(kSize, values), 1U, 1.0);
|
|
|
|
ExpectFailureCode(result, "eigen-count-inconsistent");
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, PreservesRawNegativeBoundaryAndRejectsBelowIt) {
|
|
const double threshold = -1.0e-9;
|
|
const std::vector<double> accepted_negatives{
|
|
-1.0e-12, threshold};
|
|
for (const double negative : accepted_negatives) {
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[negative](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (!call.count) {
|
|
ASSERT_EQ(outputs.eigenvalues.size(), 3U);
|
|
outputs.eigenvalues = {negative, 0.0, 1.0};
|
|
}
|
|
});
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(3U, {negative, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
1.0}),
|
|
3U, 1.0);
|
|
RequireSuccess(result);
|
|
if (!result.HasValue()) continue;
|
|
ASSERT_EQ(result.Value().eigenvalues.size(), 3U);
|
|
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[0U], negative);
|
|
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[1U], 0.0);
|
|
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[2U], 1.0);
|
|
}
|
|
|
|
const double below_threshold =
|
|
std::nextafter(threshold, -(std::numeric_limits<double>::infinity)());
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[below_threshold](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (!call.count) outputs.eigenvalues = {below_threshold, 0.0, 1.0};
|
|
});
|
|
const auto rejected = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(3U, {below_threshold, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
1.0}),
|
|
3U, 1.0);
|
|
ExpectFailureCode(rejected, "negative-eigenvalue-spectrum");
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, DoesNotRepeatUnchangedSolveAfterReturnedMismatch) {
|
|
std::size_t solve_calls = 0U;
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[&](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (!call.count) {
|
|
++solve_calls;
|
|
outputs.returned = 0;
|
|
}
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(1U, {2.0}), 1U, 1.0);
|
|
|
|
ExpectFailureCode(result, "eigen-feast-failed");
|
|
EXPECT_EQ(solve_calls, 1U);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RetriesSolveInfoTwoOnlyWithLargerInterval) {
|
|
constexpr std::size_t kSize = 11U;
|
|
std::vector<double> values(kSize * kSize, 0.0);
|
|
for (std::size_t index = 0U; index < kSize; ++index)
|
|
values[index * kSize + index] = static_cast<double>(index + 1U);
|
|
std::vector<double> solve_uppers;
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[&](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (!call.count) {
|
|
solve_uppers.push_back(call.upper);
|
|
if (solve_uppers.size() == 1U) outputs.info = 2;
|
|
}
|
|
});
|
|
|
|
const auto result =
|
|
fesa::MklFeastEigenSolver{}.Solve(MakeDenseCsr(kSize, values), 1U, 1.0);
|
|
|
|
RequireSuccess(result);
|
|
if (!result.HasValue()) return;
|
|
ASSERT_EQ(solve_uppers.size(), 2U);
|
|
EXPECT_LT(solve_uppers[0U], solve_uppers[1U]);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsNonfiniteSolveOutputWithoutRetry) {
|
|
std::size_t solve_calls = 0U;
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[&](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (!call.count) {
|
|
++solve_calls;
|
|
outputs.eigenvalues[0U] =
|
|
(std::numeric_limits<double>::quiet_NaN)();
|
|
}
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(1U, {2.0}), 1U, 1.0);
|
|
|
|
ExpectFailureCode(result, "invalid-eigenvalue-result");
|
|
EXPECT_EQ(solve_calls, 1U);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsResidualFailureWithoutSolveRetry) {
|
|
std::size_t solve_calls = 0U;
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[&](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (!call.count) {
|
|
++solve_calls;
|
|
// N=1 still requires validation of the returned all-c=2 modes.
|
|
outputs.vectors[2U] = std::sqrt(0.5);
|
|
outputs.vectors[3U] = std::sqrt(0.5);
|
|
}
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(2U, {1.0, 0.0, 0.0, 2.0}), 1U, 1.0);
|
|
|
|
ExpectFailureCode(result, "eigenpair-validation-failed");
|
|
EXPECT_NE(result.GetStatus().Diagnostics().front().message.find(
|
|
"Independent whitened residual exceeds"),
|
|
std::string::npos);
|
|
EXPECT_EQ(solve_calls, 1U);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsNonfiniteVectorAndEpsoutWithoutRetry) {
|
|
for (const bool nonfinite_vector : {true, false}) {
|
|
std::size_t solve_calls = 0U;
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[&](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (!call.count) {
|
|
++solve_calls;
|
|
if (nonfinite_vector)
|
|
outputs.vectors[0U] =
|
|
(std::numeric_limits<double>::quiet_NaN)();
|
|
else
|
|
outputs.epsout = (std::numeric_limits<double>::quiet_NaN)();
|
|
}
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(1U, {2.0}), 1U, 1.0);
|
|
|
|
ExpectFailureCode(result, "invalid-eigenvalue-result");
|
|
EXPECT_EQ(solve_calls, 1U);
|
|
}
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, RejectsGramFailureWithoutSolveRetry) {
|
|
std::size_t solve_calls = 0U;
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[&](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
if (!call.count) {
|
|
++solve_calls;
|
|
ASSERT_EQ(outputs.vectors.size(), 4U);
|
|
outputs.vectors = {1.0, 0.0, 1.0, 0.0};
|
|
}
|
|
});
|
|
|
|
const auto result = fesa::MklFeastEigenSolver{}.Solve(
|
|
MakeDenseCsr(2U, {0.0, 0.0, 0.0, 0.0}), 2U, 1.0);
|
|
|
|
ExpectFailureCode(result, "eigenpair-validation-failed");
|
|
EXPECT_EQ(solve_calls, 1U);
|
|
}
|
|
|
|
TEST(MklFeastEigenSolver, UsesCertifiedEnclosureAfterOptionalInfoOne) {
|
|
constexpr std::size_t kSize = 11U;
|
|
std::vector<double> values(kSize * kSize, 0.0);
|
|
for (std::size_t index = 0U; index < kSize; ++index)
|
|
values[index * kSize + index] = static_cast<double>(index + 1U);
|
|
fesa::internal::ScopedFeastBackendTestHook hook(
|
|
[](const fesa::internal::FeastBackendTestCall& call,
|
|
fesa::internal::FeastBackendTestOutputs& outputs) {
|
|
// The first upper guard follows the total enclosing count.
|
|
if (call.count && call.ordinal == 1U) outputs.info = 1;
|
|
});
|
|
|
|
const auto result =
|
|
fesa::MklFeastEigenSolver{}.Solve(MakeDenseCsr(kSize, values), 1U, 1.0);
|
|
|
|
RequireSuccess(result);
|
|
if (!result.HasValue()) return;
|
|
EXPECT_EQ(result.Value().upper_search_termination, "optional-status");
|
|
ASSERT_GE(result.Value().backend_calls.size(), 2U);
|
|
EXPECT_EQ(result.Value().backend_calls[1U].outcome, "optional-abandoned");
|
|
}
|