feat: complete first MITC4 FEAST eigen implementation validation

This commit is contained in:
KOKO\Mimi
2026-09-10 02:24:04 +09:00
parent 7ebdce373c
commit d5e08f868b
26 changed files with 1452 additions and 82 deletions
@@ -5,11 +5,15 @@
#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 {
@@ -54,6 +58,59 @@ void RequireSuccess(const fesa::Result<fesa::FeastEigenResult>& result) {
<< 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) {
@@ -109,15 +166,28 @@ TEST(MklFeastEigenSolver, PreservesRepeatedMultiplicityAndStableOrder) {
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);
if (!first.HasValue() || !second.HasValue()) return;
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);
}
@@ -251,3 +321,311 @@ TEST(MklFeastEigenSolver, ResolvesCloseDistinctSpectrumAfterGivensCongruence) {
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");
}