feat: complete first MITC4 FEAST eigen implementation validation
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/analysis/prepared_analysis.h"
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "fesa/model/domain.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::string StatusText(const fesa::Status& status) {
|
||||
std::string result;
|
||||
for (const auto& diagnostic : status.Diagnostics())
|
||||
result += diagnostic.code + " " + diagnostic.message + "\n";
|
||||
return result;
|
||||
}
|
||||
|
||||
fesa::Result<fesa::Domain> SharedNodeDomain(const bool reverse_source_order) {
|
||||
std::ostringstream deck;
|
||||
deck << R"inp(*Part,name=Panel
|
||||
*Node
|
||||
1,0,0,0
|
||||
2,1,0,0
|
||||
3,2,0,0
|
||||
4,0,1,0
|
||||
5,1,1,0
|
||||
6,2,1,0
|
||||
*Element,type=S4
|
||||
)inp";
|
||||
if (reverse_source_order)
|
||||
deck << "20,2,3,6,5\n10,1,2,5,4\n";
|
||||
else
|
||||
deck << "10,1,2,5,4\n20,2,3,6,5\n";
|
||||
deck << R"inp(*Elset,elset=Thin
|
||||
10
|
||||
*Elset,elset=Thick
|
||||
20
|
||||
*Shell Section,elset=Thin,material=Light
|
||||
0.5
|
||||
*Shell Section,elset=Thick,material=Heavy
|
||||
1.0
|
||||
*End Part
|
||||
*Assembly,name=Assembly
|
||||
*Instance,name=Panel-1,part=Panel
|
||||
*End Instance
|
||||
*Nset,nset=Fixed,instance=Panel-1
|
||||
1,2,3,4,6
|
||||
*Nset,nset=Shared,instance=Panel-1
|
||||
5
|
||||
*End Assembly
|
||||
*Material,name=Light
|
||||
*Elastic
|
||||
12,0
|
||||
*Density
|
||||
2
|
||||
*Material,name=Heavy
|
||||
*Elastic
|
||||
12,0
|
||||
*Density
|
||||
5
|
||||
*Boundary
|
||||
Fixed,1,6
|
||||
Shared,1,3
|
||||
Shared,5,6
|
||||
*Step,name=Step-2,nlgeom=NO,perturbation
|
||||
*Frequency
|
||||
1
|
||||
*End Step
|
||||
)inp";
|
||||
static std::atomic<unsigned long long> sequence{0U};
|
||||
const auto path = std::filesystem::temp_directory_path() /
|
||||
("fesa-eigen-mass-" +
|
||||
std::to_string(std::chrono::steady_clock::now()
|
||||
.time_since_epoch()
|
||||
.count()) +
|
||||
"-" + std::to_string(sequence.fetch_add(1U)) + ".inp");
|
||||
{
|
||||
std::ofstream stream{path};
|
||||
stream << deck.str();
|
||||
}
|
||||
auto prepared = fesa::PrepareAnalysisInput(path);
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(path, ignored);
|
||||
if (!prepared.HasValue())
|
||||
return fesa::Result<fesa::Domain>::Failure(prepared.GetStatus());
|
||||
return fesa::Result<fesa::Domain>::Success(
|
||||
std::move(prepared.Value().domain));
|
||||
}
|
||||
|
||||
struct SharedDofPencil {
|
||||
double stiffness{0.0};
|
||||
double mass{0.0};
|
||||
};
|
||||
|
||||
fesa::Result<SharedDofPencil> DirectSharedDofPencil(
|
||||
const fesa::Domain& domain) {
|
||||
const fesa::ElementFactory factory;
|
||||
SharedDofPencil result;
|
||||
for (std::size_t index = 0U; index < domain.ShellElements().Size(); ++index) {
|
||||
const auto& definition = domain.ShellElements()[index];
|
||||
const auto runtime = factory.Create(definition, domain);
|
||||
if (!runtime.HasValue())
|
||||
return fesa::Result<SharedDofPencil>::Failure(runtime.GetStatus());
|
||||
const auto* shell =
|
||||
dynamic_cast<const fesa::Mitc4Shell*>(runtime.Value().get());
|
||||
if (shell == nullptr)
|
||||
return fesa::Result<SharedDofPencil>::Failure(fesa::Status::Failure(
|
||||
fesa::FailureCategory::kModel,
|
||||
{{fesa::Severity::kError, "invalid-test-shell", {}, "ELEMENT", {},
|
||||
"Frequency mass fixture did not create an MITC4 shell."}}));
|
||||
const auto local = std::find(definition.NodeIndices().begin(),
|
||||
definition.NodeIndices().end(), 4U);
|
||||
if (local == definition.NodeIndices().end())
|
||||
return fesa::Result<SharedDofPencil>::Failure(fesa::Status::Failure(
|
||||
fesa::FailureCategory::kModel,
|
||||
{{fesa::Severity::kError, "invalid-test-shell", {}, "ELEMENT", {},
|
||||
"Frequency mass fixture lost its shared node."}}));
|
||||
const auto mass =
|
||||
shell->Mass(definition.MaterialIndex() == 0U ? 2.0 : 5.0);
|
||||
const auto stiffness = shell->Stiffness();
|
||||
if (!mass.HasValue())
|
||||
return fesa::Result<SharedDofPencil>::Failure(mass.GetStatus());
|
||||
if (!stiffness.HasValue())
|
||||
return fesa::Result<SharedDofPencil>::Failure(stiffness.GetStatus());
|
||||
const std::size_t dof =
|
||||
6U * static_cast<std::size_t>(
|
||||
std::distance(definition.NodeIndices().begin(), local)) +
|
||||
3U;
|
||||
result.mass += mass.Value().lumped_global24(dof, dof);
|
||||
result.stiffness += stiffness.Value().stabilized_global24(dof, dof);
|
||||
}
|
||||
return fesa::Result<SharedDofPencil>::Success(result);
|
||||
}
|
||||
|
||||
// E2-C06: sections and densities differ across the shared source node. The
|
||||
// one free physical rotation makes the executor's reduced pencil exactly the
|
||||
// independently scattered local diagonal sum.
|
||||
TEST(EigenMassAssembly, ScattersHeterogeneousSharedNodeMassInSourceOrder) {
|
||||
constexpr double analytic_shared_rotary_mass = 7.0 / 64.0;
|
||||
std::array<double, 2> eigenvalues{};
|
||||
for (std::size_t order = 0U; order < eigenvalues.size(); ++order) {
|
||||
const auto domain = SharedNodeDomain(order != 0U);
|
||||
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||
const auto expected = DirectSharedDofPencil(domain.Value());
|
||||
ASSERT_TRUE(expected.HasValue()) << StatusText(expected.GetStatus());
|
||||
EXPECT_LE(std::abs(expected.Value().mass - analytic_shared_rotary_mass) /
|
||||
analytic_shared_rotary_mass,
|
||||
1e-12);
|
||||
const auto state =
|
||||
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
ASSERT_TRUE(state.HasValue()) << StatusText(state.GetStatus());
|
||||
ASSERT_EQ(state.Value().free_dof_count, 1U);
|
||||
ASSERT_EQ(state.Value().modes.size(), 1U);
|
||||
const double expected_eigenvalue =
|
||||
expected.Value().stiffness / analytic_shared_rotary_mass;
|
||||
EXPECT_LE(std::abs(state.Value().modes[0U].eigenvalue - expected_eigenvalue) /
|
||||
(std::abs(state.Value().modes[0U].eigenvalue) +
|
||||
std::abs(expected_eigenvalue)),
|
||||
1e-12);
|
||||
const double amplitude = state.Value().vectors(0U, 6U * 4U + 3U);
|
||||
EXPECT_LE(std::abs(amplitude * amplitude * analytic_shared_rotary_mass -
|
||||
1.0),
|
||||
1e-12);
|
||||
eigenvalues[order] = state.Value().modes[0U].eigenvalue;
|
||||
}
|
||||
EXPECT_NEAR(eigenvalues[0U], eigenvalues[1U], 1e-12);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -12,6 +13,90 @@ namespace {
|
||||
|
||||
using Coordinates = std::array<double, 3>;
|
||||
|
||||
double FrobeniusNorm(const fesa::Matrix& matrix, const double length) {
|
||||
double norm = 0.0;
|
||||
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
|
||||
for (std::size_t column = 0U; column < matrix.Columns(); ++column) {
|
||||
const double row_scale = row % 6U < 3U ? length : 1.0;
|
||||
const double column_scale = column % 6U < 3U ? length : 1.0;
|
||||
norm = std::hypot(norm, row_scale * matrix(row, column) * column_scale);
|
||||
}
|
||||
}
|
||||
return norm;
|
||||
}
|
||||
|
||||
double MaximumEdgeLength(const std::array<fesa::Node, 4>& nodes) {
|
||||
double result = 0.0;
|
||||
for (std::size_t node = 0U; node < nodes.size(); ++node) {
|
||||
const auto& first = nodes[node].coordinates;
|
||||
const auto& second = nodes[(node + 1U) % nodes.size()].coordinates;
|
||||
result = (std::max)(result, std::hypot(
|
||||
std::hypot(first[0U] - second[0U], first[1U] - second[1U]),
|
||||
first[2U] - second[2U]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Coordinates Rotate(const Coordinates& value) {
|
||||
return {-value[1U],
|
||||
0.6 * value[0U] - 0.8 * value[2U],
|
||||
0.8 * value[0U] + 0.6 * value[2U]};
|
||||
}
|
||||
|
||||
Coordinates DirectorCross(const Coordinates& director,
|
||||
const Coordinates& rotation) {
|
||||
return {rotation[1U] * director[2U] - rotation[2U] * director[1U],
|
||||
rotation[2U] * director[0U] - rotation[0U] * director[2U],
|
||||
rotation[0U] * director[1U] - rotation[1U] * director[0U]};
|
||||
}
|
||||
|
||||
double Dot(const Coordinates& left, const Coordinates& right) {
|
||||
return left[0U] * right[0U] + left[1U] * right[1U] +
|
||||
left[2U] * right[2U];
|
||||
}
|
||||
|
||||
fesa::Matrix RotateCongruently(const fesa::Matrix& original) {
|
||||
fesa::Matrix result{24U, 24U};
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||
const std::size_t row_base = 6U * (row / 6U) +
|
||||
(row % 6U < 3U ? 0U : 3U);
|
||||
const std::size_t column_base = 6U * (column / 6U) +
|
||||
(column % 6U < 3U ? 0U : 3U);
|
||||
const std::size_t row_component = row % 3U;
|
||||
const std::size_t column_component = column % 3U;
|
||||
for (std::size_t left = 0U; left < 3U; ++left) {
|
||||
for (std::size_t right = 0U; right < 3U; ++right) {
|
||||
const Coordinates basis_left{
|
||||
left == 0U ? 1.0 : 0.0, left == 1U ? 1.0 : 0.0,
|
||||
left == 2U ? 1.0 : 0.0};
|
||||
const Coordinates basis_right{
|
||||
right == 0U ? 1.0 : 0.0, right == 1U ? 1.0 : 0.0,
|
||||
right == 2U ? 1.0 : 0.0};
|
||||
result(row, column) += Rotate(basis_left)[row_component] *
|
||||
original(row_base + left, column_base + right) *
|
||||
Rotate(basis_right)[column_component];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ExpectScaledCovariance(const fesa::Matrix& original,
|
||||
const fesa::Matrix& rotated,
|
||||
const double length) {
|
||||
const auto expected = RotateCongruently(original);
|
||||
fesa::Matrix difference{24U, 24U};
|
||||
for (std::size_t row = 0U; row < 24U; ++row)
|
||||
for (std::size_t column = 0U; column < 24U; ++column)
|
||||
difference(row, column) = rotated(row, column) - expected(row, column);
|
||||
const double ratio = FrobeniusNorm(difference, length) /
|
||||
(FrobeniusNorm(rotated, length) +
|
||||
FrobeniusNorm(expected, length));
|
||||
EXPECT_LE(ratio, 1e-12);
|
||||
}
|
||||
|
||||
fesa::Node Node(std::int64_t label, Coordinates coordinates) {
|
||||
return {{"I", label, std::to_string(label)}, coordinates, {"mass.inp", 1U}};
|
||||
}
|
||||
@@ -42,23 +127,119 @@ double Quadratic(const fesa::Matrix& matrix, const fesa::Vector& vector) {
|
||||
return vector.Dot(matrix.Multiply(vector));
|
||||
}
|
||||
|
||||
// E2-C02: values derive independently in numerical-review-evidence.md.
|
||||
// E2-C02: all values derive independently in numerical-review-evidence.md.
|
||||
TEST(Mitc4ShellMassContract, PreservesWarpedMomentsRowsAndFirstSpatialMoment) {
|
||||
const auto mass = WarpedShell().Mass(3.0);
|
||||
ASSERT_TRUE(mass.HasValue());
|
||||
const auto& value = mass.Value();
|
||||
EXPECT_NEAR(value.consistent_moments[0U](0U, 0U), 17609.0 / 28800.0, 1e-12);
|
||||
EXPECT_NEAR(value.consistent_moments[1U](0U, 0U), 11003.0 / 1440000.0, 1e-12);
|
||||
EXPECT_NEAR(value.consistent_moments[2U](0U, 0U), 88177.0 / 6912000.0, 1e-12);
|
||||
double total_m = 0.0, total_h = 0.0, total_j = 0.0;
|
||||
for (const auto& row : value.nodal_moments) {
|
||||
total_m += row[0U];
|
||||
total_h += row[1U];
|
||||
total_j += row[2U];
|
||||
const double expected[3][4][4] = {
|
||||
{{17609.0 / 28800.0, 3337.0 / 11520.0, 1547.0 / 11520.0,
|
||||
37.0 / 128.0},
|
||||
{3337.0 / 11520.0, 15793.0 / 28800.0, 7177.0 / 28800.0,
|
||||
1547.0 / 11520.0},
|
||||
{1547.0 / 11520.0, 7177.0 / 28800.0, 4297.0 / 9600.0,
|
||||
14239.0 / 57600.0},
|
||||
{37.0 / 128.0, 1547.0 / 11520.0, 14239.0 / 57600.0,
|
||||
15683.0 / 28800.0}},
|
||||
{{11003.0 / 1440000.0, 5053.0 / 1440000.0, 9839.0 / 5760000.0,
|
||||
21667.0 / 5760000.0},
|
||||
{5053.0 / 1440000.0, 27769.0 / 4320000.0, 53411.0 / 17280000.0,
|
||||
9839.0 / 5760000.0},
|
||||
{9839.0 / 5760000.0, 53411.0 / 17280000.0, 5687.0 / 960000.0,
|
||||
28673.0 / 8640000.0},
|
||||
{21667.0 / 5760000.0, 9839.0 / 5760000.0, 28673.0 / 8640000.0,
|
||||
63913.0 / 8640000.0}},
|
||||
{{88177.0 / 6912000.0, 27847.0 / 4608000.0, 7747.0 / 2764800.0,
|
||||
41693.0 / 6912000.0},
|
||||
{27847.0 / 4608000.0, 1757.0 / 153600.0, 1331.0 / 256000.0,
|
||||
7747.0 / 2764800.0},
|
||||
{7747.0 / 2764800.0, 1331.0 / 256000.0, 21521.0 / 2304000.0,
|
||||
23773.0 / 4608000.0},
|
||||
{41693.0 / 6912000.0, 7747.0 / 2764800.0, 23773.0 / 4608000.0,
|
||||
5237.0 / 460800.0}}};
|
||||
constexpr double element_mass = 7743.0 / 1600.0;
|
||||
constexpr double half_thickness = 0.25;
|
||||
for (std::size_t moment = 0U; moment < 3U; ++moment) {
|
||||
const double scale = element_mass * std::pow(half_thickness, moment);
|
||||
for (std::size_t row = 0U; row < 4U; ++row) {
|
||||
double expected_row = 0.0;
|
||||
for (std::size_t column = 0U; column < 4U; ++column) {
|
||||
EXPECT_LE(std::abs(value.consistent_moments[moment](row, column) -
|
||||
expected[moment][row][column]) /
|
||||
scale,
|
||||
1e-12)
|
||||
<< "moment=" << moment << " row=" << row
|
||||
<< " column=" << column;
|
||||
expected_row += expected[moment][row][column];
|
||||
}
|
||||
EXPECT_LE(std::abs(value.nodal_moments[row][moment] - expected_row) /
|
||||
scale,
|
||||
1e-12)
|
||||
<< "moment=" << moment << " row=" << row;
|
||||
}
|
||||
}
|
||||
EXPECT_NEAR(total_m, 7743.0 / 1600.0, 1e-12);
|
||||
EXPECT_NEAR(total_h, 739.0 / 12000.0, 1e-12);
|
||||
EXPECT_NEAR(total_j, 517.0 / 5120.0, 1e-12);
|
||||
const std::array<Coordinates, 4> coordinates{{
|
||||
{{-1.0, -1.0, 0.1}}, {{1.0, -1.0, -0.1}},
|
||||
{{1.0, 1.0, 0.2}}, {{-1.0, 1.0, -0.1}}}};
|
||||
const std::array<Coordinates, 4> directors{{
|
||||
{{0.0, 0.0, 1.0}}, {{0.6, 0.0, 0.8}},
|
||||
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}}, {{0.0, 0.6, 0.8}}}};
|
||||
const Coordinates reference = coordinates.front();
|
||||
const Coordinates expected_first{{-79747.0 / 360000.0,
|
||||
-685951.0 / 2880000.0,
|
||||
49651.0 / 320000.0}};
|
||||
Coordinates actual_first{};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
actual_first[component] +=
|
||||
value.nodal_moments[node][0U] *
|
||||
(coordinates[node][component] - reference[component]) +
|
||||
value.nodal_moments[node][1U] * directors[node][component];
|
||||
}
|
||||
}
|
||||
double difference = 0.0;
|
||||
for (std::size_t component = 0U; component < 3U; ++component)
|
||||
difference = std::hypot(difference, actual_first[component] -
|
||||
(expected_first[component] -
|
||||
element_mass * reference[component]));
|
||||
EXPECT_LE(difference / (element_mass * std::hypot(2.0, 0.3)), 1e-12);
|
||||
}
|
||||
|
||||
// E2-C05: rebuild both masses after a proper rotation and translation.
|
||||
TEST(Mitc4ShellMassContract, RecomputesWarpedMassWithScaledCovariance) {
|
||||
const std::array<fesa::Node, 4> nodes{
|
||||
Node(1, {-1.0, -1.0, 0.1}), Node(2, {1.0, -1.0, -0.1}),
|
||||
Node(3, {1.0, 1.0, 0.2}), Node(4, {-1.0, 1.0, -0.1})};
|
||||
const std::array<Coordinates, 4> directors{{
|
||||
{{0.0, 0.0, 1.0}}, {{0.6, 0.0, 0.8}},
|
||||
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}}, {{0.0, 0.6, 0.8}}}};
|
||||
std::array<fesa::Node, 4> transformed_nodes = nodes;
|
||||
std::array<Coordinates, 4> transformed_directors{};
|
||||
for (std::size_t node = 0U; node < nodes.size(); ++node) {
|
||||
transformed_nodes[node].coordinates = Rotate(nodes[node].coordinates);
|
||||
transformed_nodes[node].coordinates[0U] += 7.0;
|
||||
transformed_nodes[node].coordinates[1U] -= 3.0;
|
||||
transformed_nodes[node].coordinates[2U] += 5.0;
|
||||
transformed_directors[node] = Rotate(directors[node]);
|
||||
}
|
||||
const fesa::ShellSection section{"S", 0.5, 0U, {"mass.inp", 2U}};
|
||||
const fesa::LinearElasticMaterial material{"M", 12.0, 0.0,
|
||||
{"mass.inp", 3U}};
|
||||
const auto original = fesa::Mitc4Shell::Create(Pointers(nodes), directors,
|
||||
section, material);
|
||||
const auto transformed = fesa::Mitc4Shell::Create(
|
||||
Pointers(transformed_nodes), transformed_directors, section, material);
|
||||
ASSERT_TRUE(original.HasValue());
|
||||
ASSERT_TRUE(transformed.HasValue());
|
||||
const auto original_mass = original.Value().Mass(3.0);
|
||||
const auto transformed_mass = transformed.Value().Mass(3.0);
|
||||
ASSERT_TRUE(original_mass.HasValue());
|
||||
ASSERT_TRUE(transformed_mass.HasValue());
|
||||
const double length = MaximumEdgeLength(nodes);
|
||||
ExpectScaledCovariance(original_mass.Value().consistent_global24,
|
||||
transformed_mass.Value().consistent_global24, length);
|
||||
ExpectScaledCovariance(original_mass.Value().lumped_global24,
|
||||
transformed_mass.Value().lumped_global24, length);
|
||||
}
|
||||
|
||||
// E2-C03: scalar row-sum lumping retains uniform translation energy and is
|
||||
@@ -80,6 +261,142 @@ TEST(Mitc4ShellMassContract, SatisfiesUniformAndJensenEnergyContracts) {
|
||||
Quadratic(mass.Value().consistent_global24, field));
|
||||
}
|
||||
|
||||
// E2-C03: construct both 24-by-24 mass variants from the independently
|
||||
// checked scalar moments, including the C1 translation/rotation coupling.
|
||||
TEST(Mitc4ShellMassContract, MapsScalarMomentsIntoBothMassBlocks) {
|
||||
const auto mass = WarpedShell().Mass(3.0);
|
||||
ASSERT_TRUE(mass.HasValue());
|
||||
const std::array<Coordinates, 4> directors{{
|
||||
{{0.0, 0.0, 1.0}}, {{0.6, 0.0, 0.8}},
|
||||
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}}, {{0.0, 0.6, 0.8}}}};
|
||||
fesa::Matrix expected_consistent{24U, 24U};
|
||||
fesa::Matrix expected_lumped{24U, 24U};
|
||||
for (std::size_t first = 0U; first < 4U; ++first) {
|
||||
for (std::size_t second = 0U; second < 4U; ++second) {
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
for (std::size_t column = 0U; column < 3U; ++column) {
|
||||
const double identity = row == column ? 1.0 : 0.0;
|
||||
const Coordinates unit_column{{column == 0U ? 1.0 : 0.0,
|
||||
column == 1U ? 1.0 : 0.0,
|
||||
column == 2U ? 1.0 : 0.0}};
|
||||
const Coordinates unit_row{{row == 0U ? 1.0 : 0.0,
|
||||
row == 1U ? 1.0 : 0.0,
|
||||
row == 2U ? 1.0 : 0.0}};
|
||||
const double sj = DirectorCross(directors[second], unit_column)[row];
|
||||
const double si = DirectorCross(directors[first], unit_row)[column];
|
||||
expected_consistent(6U * first + row, 6U * second + column) =
|
||||
mass.Value().consistent_moments[0U](first, second) * identity;
|
||||
expected_consistent(6U * first + row, 6U * second + 3U + column) =
|
||||
mass.Value().consistent_moments[1U](first, second) * sj;
|
||||
expected_consistent(6U * first + 3U + row, 6U * second + column) =
|
||||
mass.Value().consistent_moments[1U](first, second) * si;
|
||||
expected_consistent(6U * first + 3U + row,
|
||||
6U * second + 3U + column) =
|
||||
mass.Value().consistent_moments[2U](first, second) *
|
||||
Dot(DirectorCross(directors[first], unit_row),
|
||||
DirectorCross(directors[second], unit_column));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
for (std::size_t column = 0U; column < 3U; ++column) {
|
||||
const double identity = row == column ? 1.0 : 0.0;
|
||||
const Coordinates unit_column{{column == 0U ? 1.0 : 0.0,
|
||||
column == 1U ? 1.0 : 0.0,
|
||||
column == 2U ? 1.0 : 0.0}};
|
||||
const Coordinates unit_row{{row == 0U ? 1.0 : 0.0,
|
||||
row == 1U ? 1.0 : 0.0,
|
||||
row == 2U ? 1.0 : 0.0}};
|
||||
const double sj = DirectorCross(directors[first], unit_column)[row];
|
||||
const double si = DirectorCross(directors[first], unit_row)[column];
|
||||
expected_lumped(6U * first + row, 6U * first + column) =
|
||||
mass.Value().nodal_moments[first][0U] * identity;
|
||||
expected_lumped(6U * first + row, 6U * first + 3U + column) =
|
||||
mass.Value().nodal_moments[first][1U] * sj;
|
||||
expected_lumped(6U * first + 3U + row, 6U * first + column) =
|
||||
mass.Value().nodal_moments[first][1U] * si;
|
||||
expected_lumped(6U * first + 3U + row, 6U * first + 3U + column) =
|
||||
mass.Value().nodal_moments[first][2U] *
|
||||
Dot(DirectorCross(directors[first], unit_row),
|
||||
DirectorCross(directors[first], unit_column));
|
||||
}
|
||||
}
|
||||
}
|
||||
fesa::Vector velocity{24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
velocity[6U * node] = static_cast<double>(node + 1U);
|
||||
velocity[6U * node + 1U] = -0.5 * static_cast<double>(node);
|
||||
velocity[6U * node + 3U] = 0.25;
|
||||
}
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||
EXPECT_NEAR(mass.Value().consistent_global24(row, column),
|
||||
expected_consistent(row, column), 1e-12);
|
||||
EXPECT_NEAR(mass.Value().lumped_global24(row, column),
|
||||
expected_lumped(row, column), 1e-12);
|
||||
}
|
||||
}
|
||||
fesa::Matrix consistent_difference{24U, 24U};
|
||||
fesa::Matrix lumped_difference{24U, 24U};
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||
consistent_difference(row, column) =
|
||||
mass.Value().consistent_global24(row, column) -
|
||||
expected_consistent(row, column);
|
||||
lumped_difference(row, column) = mass.Value().lumped_global24(row, column) -
|
||||
expected_lumped(row, column);
|
||||
}
|
||||
}
|
||||
const double length = std::hypot(2.0, 0.3);
|
||||
EXPECT_LE(FrobeniusNorm(consistent_difference, length) /
|
||||
(FrobeniusNorm(mass.Value().consistent_global24, length) +
|
||||
FrobeniusNorm(expected_consistent, length)),
|
||||
1e-12);
|
||||
EXPECT_LE(FrobeniusNorm(lumped_difference, length) /
|
||||
(FrobeniusNorm(mass.Value().lumped_global24, length) +
|
||||
FrobeniusNorm(expected_lumped, length)),
|
||||
1e-12);
|
||||
double pairwise_work = 0.0;
|
||||
for (std::size_t first = 0U; first < 4U; ++first) {
|
||||
for (std::size_t second = 0U; second < 4U; ++second) {
|
||||
Coordinates displacement_difference{};
|
||||
Coordinates director_velocity_difference{};
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
displacement_difference[component] =
|
||||
velocity[6U * first + component] -
|
||||
velocity[6U * second + component];
|
||||
}
|
||||
Coordinates first_rotation{{velocity[6U * first + 3U],
|
||||
velocity[6U * first + 4U],
|
||||
velocity[6U * first + 5U]}};
|
||||
Coordinates second_rotation{{velocity[6U * second + 3U],
|
||||
velocity[6U * second + 4U],
|
||||
velocity[6U * second + 5U]}};
|
||||
const auto first_director_velocity =
|
||||
DirectorCross(directors[first], first_rotation);
|
||||
const auto second_director_velocity =
|
||||
DirectorCross(directors[second], second_rotation);
|
||||
for (std::size_t component = 0U; component < 3U; ++component)
|
||||
director_velocity_difference[component] =
|
||||
first_director_velocity[component] -
|
||||
second_director_velocity[component];
|
||||
pairwise_work += 0.5 * (
|
||||
mass.Value().consistent_moments[0U](first, second) *
|
||||
Dot(displacement_difference, displacement_difference) +
|
||||
2.0 * mass.Value().consistent_moments[1U](first, second) *
|
||||
Dot(displacement_difference, director_velocity_difference) +
|
||||
mass.Value().consistent_moments[2U](first, second) *
|
||||
Dot(director_velocity_difference, director_velocity_difference));
|
||||
}
|
||||
}
|
||||
const double consistent_work =
|
||||
Quadratic(mass.Value().consistent_global24, velocity);
|
||||
const double lumped_work = Quadratic(mass.Value().lumped_global24, velocity);
|
||||
EXPECT_LE(std::abs((lumped_work - consistent_work) - pairwise_work) /
|
||||
(lumped_work + consistent_work),
|
||||
1e-12);
|
||||
}
|
||||
|
||||
// E2-C04: assemble the physical velocity work over successively finer meshes.
|
||||
TEST(Mitc4ShellMassContract, ReproducesAffineSquareVelocityWork) {
|
||||
const std::array<Coordinates, 4> directors{{{{0.0, 0.0, 1.0}},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#define NOMINMAX
|
||||
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
|
||||
|
||||
#include <Windows.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <hdf5.h>
|
||||
|
||||
@@ -113,6 +115,22 @@ TEST(EigenHdf5ResultsWriter, WritesModalSchemaAndRejectsTamperedState) {
|
||||
const std::string retained{std::istreambuf_iterator<char>{retained_file}, {}};
|
||||
EXPECT_TRUE(retained == original);
|
||||
retained_file.close();
|
||||
// Denying delete sharing forces the actual atomic replacement to fail after
|
||||
// writing and validating a fresh candidate, while retaining a valid final.
|
||||
const HANDLE lock = CreateFileW(path.c_str(), GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
ASSERT_NE(lock, INVALID_HANDLE_VALUE);
|
||||
const auto locked_write = writer.Write(path, domain, state, {});
|
||||
EXPECT_NE(CloseHandle(lock), 0);
|
||||
ASSERT_FALSE(locked_write.IsOk());
|
||||
ASSERT_FALSE(locked_write.Diagnostics().empty());
|
||||
EXPECT_EQ(locked_write.Diagnostics()[0].code, "hdf5-finalization-failure");
|
||||
std::ifstream locked_final{path, std::ios::binary};
|
||||
const std::string after_failure{std::istreambuf_iterator<char>{locked_final},
|
||||
{}};
|
||||
EXPECT_EQ(after_failure, original);
|
||||
locked_final.close();
|
||||
ASSERT_TRUE(
|
||||
fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
|
||||
const hid_t corrupt_file =
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user