#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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::max)() / left; } bool FitsDoubleStorage(const std::size_t count) { return count <= std::vector{}.max_size() && count <= static_cast( (std::numeric_limits::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(*info), static_cast(*m0), static_cast(*returned), static_cast(*loop), *epsout, std::vector(eigenvalues, eigenvalues + eigenvalue_count), std::vector(vectors, vectors + vector_count), std::vector(residuals, residuals + residual_count)}; (*g_feast_backend_test_hook)(call, outputs); *info = static_cast(outputs.info); *m0 = static_cast(outputs.m0); *returned = static_cast(outputs.returned); *loop = static_cast(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::numeric_limits::max)()); } double NextUp(const double value) { return std::nextafter(value, (std::numeric_limits::infinity)()); } double Delta(const double value, const double beta) { const double up = std::nextafter(value, (std::numeric_limits::infinity)()); const double down = std::nextafter(value, -(std::numeric_limits::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(mkl_set_num_threads_local(previous_)); } private: int previous_; }; struct Csr { std::vector offsets; std::vector columns; std::vector values; }; Result 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::Failure(Failure( "feast-integer-overflow", "csr-index", "The public CSR index exceeds the oneMKL ILP64 integer range.")); } copy.columns.push_back(static_cast(column + 1U)); copy.values.push_back(values[position] / scale); } if (!FitsMkl(copy.values.size() + 1U)) { return Result::Failure(Failure("feast-integer-overflow", "csr-offset", "The converted one-based CSR offset " "exceeds the oneMKL integer range.")); } copy.offsets.push_back(static_cast(copy.values.size() + 1U)); } return Result::Success(std::move(copy)); } /// @brief Keeps the independently scaled full or upper-mirrored operator. Result ScaledActionMatrix(const SparseMatrix& matrix, const double scale, const bool mirror_upper = false) { std::vector contributions; std::vector> 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 IdentityOneBased(const std::size_t size) { if (!FitsMkl(size) || !FitsMkl(size + 1U)) { return Result::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(index + 1U)); if (index < size) { identity.columns.push_back(static_cast(index + 1U)); } } return Result::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(offsets[column]); const auto end = columns.begin() + static_cast(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(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& 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& 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& 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(state >> 11U) * 0x1.0p-53 - 1.0; } std::vector> reflectors; std::vector 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 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(columns)) <= 1e-12; } } // namespace Result 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::Failure( Failure("eigen-allocation-failed", "generalized-workspace", "Allocation of generalized sparse-pencil storage failed.")); } } Result 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 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::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::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::Failure(mass_symmetry); MklPardisoSolver spd_check; const Status spd = spd_check.Factorize(*mass); if (!spd.IsOk()) return Result::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::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::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::Failure(Failure( "eigen-allocation-failed", "count-workspace", "The required count workspace exceeds addressable double storage.")); } const MKL_INT mkl_n = static_cast(n); const Status symmetry = ValidateSymmetry(whitened_operator); if (!symmetry.IsOk()) return Result::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::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::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::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::Failure(a.GetStatus()); auto b = mass != nullptr ? CopyUpperOneBased(*mass, 1.0) : IdentityOneBased(n); if (!b.HasValue()) return Result::Failure(b.GetStatus()); auto action_matrix = ScaledActionMatrix(whitened_operator, operator_scale); if (!action_matrix.HasValue()) return Result::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::infinity)()); upper = NextUp(g_upper + 16.0 * Delta(g_upper, beta)); } else if (!zero_operator) { g_lower = (std::numeric_limits::infinity)(); g_upper = -(std::numeric_limits::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::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::infinity)()); upper = NextUp(g_upper + 16.0 * upper_delta); } if (!std::isfinite(lower) || !std::isfinite(upper) || !(lower < upper)) { return Result::Failure( Failure("feast-invalid-enclosure", "gershgorin", "A strict finite FEAST enclosure could not be constructed.")); } LocalThreadGuard threads; std::vector 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 snapshot{}; SetControls(snapshot, false); std::vector base_fpm; base_fpm.reserve(snapshot.size()); for (const MKL_INT value : snapshot) base_fpm.push_back(static_cast(value)); std::vector count_e; std::vector count_res; std::vector 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::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 fpm{}; SetControls(fpm, true); MKL_INT m0 = static_cast(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(m0) <= n; FeastBackendCall event{calls.size(), "count", purpose, call_lower, call_upper, n, static_cast(info), count_available, 0U, false, 0, false, 0.0, "", ""}; if (count_available) { event.count = static_cast(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(m0) > n) { return TraceFailure("eigen-count-inconsistent", "count-output", "FEAST count output is outside [0,n]."); } *count = static_cast(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(attempt)); lower = -extent; upper = extent; } else { const double multiplier = std::ldexp(1.0, static_cast(attempt)); lower = std::nextafter(g_lower - 16.0 * multiplier * Delta(g_lower, beta), -(std::numeric_limits::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::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 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::Failure(prefix_status); if (negative_count != 0U) return Result::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 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 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 guards{candidate - margin, candidate, candidate + margin}; if (!(fixed_lower < guards[0U] && guards[2U] < enclosure_upper)) continue; std::array 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::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::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::Failure( TraceFailure("eigen-count-inconsistent", "final-guard", "The final interior endpoint is not guarded by equal " "successful counts.")); } } if (count > (std::numeric_limits::max)() - 8U || count > ((std::numeric_limits::max)() - 1U) / 3U) { return Result::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::Failure( TraceFailure("feast-workspace-overflow", "solve-workspace", "The FEAST solve workspace size overflows.")); if (!internal::FitsDoubleStorage(n * capacity) || !internal::FitsDoubleStorage(capacity)) { return Result::Failure(TraceFailure( "eigen-allocation-failed", "solve-workspace", "The required solve workspace exceeds addressable double storage.")); } std::vector eigenvalues; std::vector residuals; std::vector 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::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::Failure(TraceFailure( "eigen-allocation-failed", "solve-workspace", "Allocation of the required FEAST solve workspace failed.")); } if (!FillInitialBasis(vectors, n, solve_capacity)) { return Result::Failure(TraceFailure( "eigenpair-validation-failed", "solve-initial-basis", "The prescribed solve initial basis failed QR validation.")); } std::array fpm{}; SetControls(fpm, false); MKL_INT m0 = static_cast(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(returned) <= solve_capacity; calls.push_back( {calls.size(), "solve", "solve", fixed_lower, solve_upper, solve_capacity, static_cast(info), solve_outputs_available, solve_outputs_available ? static_cast(returned) : 0U, solve_outputs_available, solve_outputs_available ? static_cast(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::Failure( TraceFailure("invalid-eigenvalue-result", "solve-output", "A successful FEAST solve returned nonfinite epsout or " "invalid loop.")); } if (info == 0 && returned >= 0 && static_cast(returned) <= solve_capacity) { for (std::size_t column = 0U; column < static_cast(returned); ++column) { if (!std::isfinite(eigenvalues[column])) return Result::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::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(returned) == count; if (endpoint_separated) { for (std::size_t index = 0U; index < count; ++index) { if (!std::isfinite(eigenvalues[index])) { return Result::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(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::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::Failure(TraceFailure( "eigen-count-inconsistent", "solve-retry-guard", "The larger retry interval is not guarded by matching counts.")); } } if (count > (std::numeric_limits::max)() - 8U || count > ((std::numeric_limits::max)() - 1U) / 3U) { return Result::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::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::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::Failure(TraceFailure( "invalid-eigenvalue-result", std::to_string(index), "FEAST returned a nonfinite eigenvector before sorting.")); } } } std::vector 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 ordered_values; std::vector ordered_backend_available; std::vector ordered_backend_res; std::vector ordered_mu; std::vector 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::Failure( TraceFailure("invalid-eigenvalue-result", std::to_string(input), "FEAST returned a nonfinite eigenvalue.")); } if (mu < -1.0e-9) { return Result::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::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::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::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::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::infinity)()) : std::sqrt(residual_norm) / residual_denominator; if (!std::isfinite(rho_b) || rho_b > 1.0e-10) { if (mass != nullptr) return Result::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::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::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::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::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::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::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(count))); if (!std::isfinite(gram_error) || gram_error > 1.0e-10) { return Result::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(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::Success(std::move(result)); } catch (const std::invalid_argument&) { return Result::Failure( Failure("eigenpair-validation-failed", "independent-action", "Modal product inputs must be finite and shape compatible.")); } catch (const std::length_error&) { return Result::Failure( Failure("feast-integer-overflow", "independent-action", "Independent sparse action exceeds the backend range.")); } catch (const std::runtime_error&) { return Result::Failure( Failure("eigen-multiplication-failed", "independent-action", "Independent sparse action failed.")); } } // namespace fesa