feat: add eigen2 frequency pipeline and fail-closed validation
Implement MITC4 mass, constrained whitening, MKL FEAST, modal HDF5, and deterministic Value comparison. MSVC Debug builds; 394 of 395 tests pass. Original-model all-c residual and the documented S3 static contract conflict remain unresolved; this is an implementation checkpoint, not feature acceptance.
This commit is contained in:
@@ -6,6 +6,7 @@ add_executable(
|
||||
unit/analysis/analysis_test.cpp
|
||||
unit/analysis/analysis_model_test.cpp
|
||||
unit/analysis/analysis_state_test.cpp
|
||||
unit/analysis/eigen_reduction_test.cpp
|
||||
unit/analysis/nonlinear_analysis_state_test.cpp
|
||||
unit/analysis/nonlinear_static_control_test.cpp
|
||||
unit/analysis/newton_iteration_log_test.cpp
|
||||
@@ -24,6 +25,7 @@ add_executable(
|
||||
unit/elements/euler_beam_3d_test.cpp
|
||||
unit/elements/finite_rotation_test.cpp
|
||||
unit/elements/mitc4_shell_test.cpp
|
||||
unit/elements/mitc4_shell_mass_contract_test.cpp
|
||||
unit/elements/nonlinear_mitc4_shell_test.cpp
|
||||
unit/elements/nonlinear_mitc4_shell_tangent_test.cpp
|
||||
unit/fem/dof_manager_test.cpp
|
||||
@@ -34,10 +36,12 @@ add_executable(
|
||||
unit/math/vector_test.cpp
|
||||
unit/io/abaqus/domain_mapping_components_test.cpp
|
||||
unit/io/abaqus/domain_mapper_test.cpp
|
||||
unit/io/abaqus/eigen_domain_test.cpp
|
||||
unit/io/abaqus/input_reader_test.cpp
|
||||
unit/io/abaqus/input_syntax_test.cpp
|
||||
unit/io/abaqus/shell_surface_pressure_domain_test.cpp
|
||||
unit/io/hdf5/hdf5_results_writer_test.cpp
|
||||
unit/io/hdf5/eigen_hdf5_results_writer_test.cpp
|
||||
unit/io/hdf5/nonlinear_hdf5_results_writer_test.cpp
|
||||
unit/io/hdf5/hdf5_writer_components_test.cpp
|
||||
unit/loads/load_test.cpp
|
||||
@@ -52,6 +56,7 @@ add_executable(
|
||||
unit/results/result_recovery_components_test.cpp
|
||||
unit/results/result_recovery_test.cpp
|
||||
unit/results/results_writer_test.cpp
|
||||
unit/solvers/eigen/mkl_feast_eigen_solver_test.cpp
|
||||
unit/solvers/linear/linear_solver_test.cpp
|
||||
unit/solvers/linear/mkl_pardiso_solver_test.cpp
|
||||
unit/solvers/linear/tangent_solver_test.cpp
|
||||
@@ -75,10 +80,12 @@ target_link_libraries(
|
||||
add_executable(
|
||||
fesa_integration_tests
|
||||
integration/analysis/linear_static_analysis_test.cpp
|
||||
integration/analysis/linear_frequency_procedure_test.cpp
|
||||
integration/analysis/shell_surface_pressure_linear_test.cpp
|
||||
integration/analysis/shell_surface_pressure_nonlinear_test.cpp
|
||||
integration/analysis/nonlinear_static_procedure_test.cpp
|
||||
integration/app/fesa_application_test.cpp
|
||||
integration/app/eigen_application_test.cpp
|
||||
integration/app/shell_surface_pressure_application_test.cpp
|
||||
)
|
||||
|
||||
@@ -93,6 +100,9 @@ target_link_libraries(
|
||||
add_executable(
|
||||
fesa_reference_tests
|
||||
reference/reference_tolerance_policy.cpp
|
||||
reference/eigen_reference_comparison.cpp
|
||||
reference/eigen_reference_comparison_test.cpp
|
||||
reference/eigen_reference_cases_test.cpp
|
||||
reference/reference_tolerance_policy_test.cpp
|
||||
reference/reference_comparison.cpp
|
||||
reference/reference_comparison_test.cpp
|
||||
@@ -178,18 +188,21 @@ endif()
|
||||
|
||||
gtest_discover_tests(
|
||||
fesa_unit_tests
|
||||
DISCOVERY_MODE PRE_TEST
|
||||
PROPERTIES
|
||||
LABELS "linear-static-3d-euler-beam;unit"
|
||||
)
|
||||
|
||||
gtest_discover_tests(
|
||||
fesa_integration_tests
|
||||
DISCOVERY_MODE PRE_TEST
|
||||
PROPERTIES
|
||||
LABELS "linear-static-3d-euler-beam;integration"
|
||||
)
|
||||
|
||||
gtest_discover_tests(
|
||||
fesa_reference_tests
|
||||
DISCOVERY_MODE PRE_TEST
|
||||
PROPERTIES
|
||||
LABELS "linear-static-3d-euler-beam;reference"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/analysis/linear_static_procedure.h"
|
||||
#include "fesa/analysis/prepared_analysis.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/assembly/sparse_assembler.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/model/domain.h"
|
||||
#include "fesa/model/model_types.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
|
||||
TEST(LinearFrequencyProcedureExecutor, RejectsDomainWithoutFrequencyStep) {
|
||||
fesa::ModelDefinition definition;
|
||||
definition.source_path = "frequency-missing.inp";
|
||||
definition.source_content_identity = "frequency-missing";
|
||||
auto domain = fesa::Domain::Create(std::move(definition));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
|
||||
const fesa::LinearFrequencyProcedureExecutor executor;
|
||||
const auto result = executor.Execute(domain.Value());
|
||||
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kSolver);
|
||||
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U);
|
||||
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
|
||||
"invalid-frequency-procedure");
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string StatusText(const fesa::Status& status) {
|
||||
std::string text;
|
||||
for (const auto& d : status.Diagnostics())
|
||||
text += d.code + " " + d.message + "\n";
|
||||
return text;
|
||||
}
|
||||
|
||||
fesa::Result<fesa::Domain> PanelDomain(bool rotated, bool free_shell,
|
||||
bool statics = false,
|
||||
std::size_t requested = 1U,
|
||||
double length_scale = 1.0,
|
||||
double density = 1.0,
|
||||
double modulus = 12.0) {
|
||||
const std::array<std::array<double, 3>, 4> points{{{{0.8, -2.0, -0.6}},
|
||||
{{0.8, 2.0, -0.6}},
|
||||
{{-0.8, 2.0, 0.6}},
|
||||
{{-0.8, -2.0, 0.6}}}};
|
||||
std::ostringstream deck;
|
||||
deck << std::setprecision(17) << "*Part,name=Panel\n*Node\n";
|
||||
const double c = std::sqrt(0.5);
|
||||
for (std::size_t i = 0; i < points.size(); ++i) {
|
||||
const auto& p = points[i];
|
||||
deck << 11U * (i + 1U) << ','
|
||||
<< length_scale * (rotated ? c * (p[0] - p[1]) : p[0]) << ','
|
||||
<< length_scale * (rotated ? c * (p[0] + p[1]) : p[1]) << ','
|
||||
<< length_scale * p[2] << '\n';
|
||||
}
|
||||
deck << R"inp(*Element,type=S4
|
||||
71,11,22,33,44
|
||||
*Elset,elset=Shell
|
||||
71
|
||||
*Shell Section,elset=Shell,material=Steel
|
||||
)inp" << length_scale
|
||||
<< R"inp(
|
||||
*End Part
|
||||
*Assembly,name=Assembly
|
||||
*Instance,name=Panel-1,part=Panel
|
||||
*End Instance
|
||||
*Nset,nset=Fixed,instance=Panel-1
|
||||
22,33,44
|
||||
*Nset,nset=Tip,instance=Panel-1
|
||||
11
|
||||
*End Assembly
|
||||
*Material,name=Steel
|
||||
*Elastic
|
||||
)inp" << modulus
|
||||
<< ",0.\n";
|
||||
if (!statics) deck << "*Density\n" << density << '\n';
|
||||
if (!free_shell) deck << "*Boundary\nFixed,1,6\nTip,1,5\n";
|
||||
if (statics) {
|
||||
deck << "*Step,name=Static,nlgeom=NO\n*Static\n0.1,1.,0.01,1.\n*Cload\nTip,"
|
||||
"4,"
|
||||
<< (-4.0 / 3.0) * (rotated ? c : 1.0) << "\nTip,5,"
|
||||
<< (rotated ? (-4.0 / 3.0) * c : 0.0) << "\nTip,6,1.\n";
|
||||
} else
|
||||
deck << "*Step,name=Step-2,nlgeom=NO,perturbation\n*Frequency\n"
|
||||
<< requested << '\n';
|
||||
deck << "*End Step\n";
|
||||
static std::atomic<unsigned long long> sequence{0U};
|
||||
const auto path =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("fesa-eigen-procedure-" +
|
||||
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));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(LinearFrequencyProcedureExecutor,
|
||||
MatchesPartialBoundaryPencilAndFullResidualCovariance) {
|
||||
std::array<double, 24> original_residual{};
|
||||
for (const bool rotated : {false, true}) {
|
||||
auto domain = PanelDomain(rotated, false);
|
||||
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||
const auto result =
|
||||
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
|
||||
const auto& state = result.Value();
|
||||
ASSERT_EQ(state.modes.size(), 1U);
|
||||
EXPECT_EQ(state.mass_null_dimension, 0U);
|
||||
EXPECT_EQ(state.finite_dimension, 1U);
|
||||
EXPECT_NEAR(state.modes[0].eigenvalue, 10369.0 / 450.0, 1e-10);
|
||||
EXPECT_EQ(state.modes[0].classification, 1U);
|
||||
EXPECT_EQ(state.vectors.Rows(), 1U);
|
||||
EXPECT_EQ(state.vectors.Columns(), 24U);
|
||||
EXPECT_NEAR(state.vectors(0U, 5U), std::sqrt(50.0 / 3.0), 1e-12);
|
||||
for (std::size_t dof = 0; dof < 24U; ++dof)
|
||||
if (dof != 5U) EXPECT_EQ(state.vectors(0U, dof), 0.0);
|
||||
ASSERT_EQ(state.eigenpair_checks.size(), 1U);
|
||||
EXPECT_NEAR(state.eigenpair_checks[0].modal_mass, 1.0, 1e-12);
|
||||
EXPECT_LE(state.eigenpair_checks[0].eta_r, 1e-10);
|
||||
EXPECT_LE(state.eigenpair_checks[0].eta_energy, 1e-10);
|
||||
EXPECT_LE(state.eigenpair_checks[0].rho_b, 1e-10);
|
||||
EXPECT_EQ(state.backend.eigenvectors.Rows(), 0U);
|
||||
auto static_domain = PanelDomain(rotated, false, true);
|
||||
ASSERT_TRUE(static_domain.HasValue())
|
||||
<< StatusText(static_domain.GetStatus());
|
||||
const fesa::SerialParallelFor serial;
|
||||
const auto model = fesa::AnalysisModel::Create(static_domain.Value());
|
||||
ASSERT_TRUE(model.HasValue());
|
||||
const auto dofs = fesa::DofManager::Create(model.Value());
|
||||
ASSERT_TRUE(dofs.HasValue());
|
||||
const auto stiffness = fesa::SparseAssembler::AssembleStiffness(
|
||||
model.Value(), dofs.Value(), serial);
|
||||
ASSERT_TRUE(stiffness.HasValue());
|
||||
fesa::Vector unit{24U};
|
||||
unit[5U] = 1.0;
|
||||
const double kff = stiffness.Value().Multiply(unit)[5U];
|
||||
EXPECT_NEAR(kff, 10369.0 / 7500.0, 1e-12);
|
||||
fesa::Vector displacement{24U};
|
||||
displacement[5U] = 1.0 / kff;
|
||||
EXPECT_NEAR(displacement[5U], 7500.0 / 10369.0, 1e-12);
|
||||
auto residual = stiffness.Value().Multiply(displacement);
|
||||
const double c = std::sqrt(0.5);
|
||||
residual[3U] -= (-4.0 / 3.0) * (rotated ? c : 1.0);
|
||||
residual[4U] -= rotated ? (-4.0 / 3.0) * c : 0.0;
|
||||
residual[5U] -= 1.0;
|
||||
EXPECT_NEAR(residual[5U], 0.0, 1e-12);
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
if (!rotated)
|
||||
original_residual[i] = residual[i];
|
||||
else {
|
||||
const std::size_t base = (i / 3U) * 3U;
|
||||
const double expected =
|
||||
i % 3U == 0U
|
||||
? c * (original_residual[base] - original_residual[base + 1U])
|
||||
: (i % 3U == 1U ? c * (original_residual[base] +
|
||||
original_residual[base + 1U])
|
||||
: original_residual[i]);
|
||||
EXPECT_NEAR(residual[i], expected, 1e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// E2-C09 is a pencil oracle. The public static contract conflict is recorded
|
||||
// separately; direct covariance does not certify spatial moment equilibrium.
|
||||
TEST(LinearFrequencyProcedureExecutor,
|
||||
PartialBoundaryStaticRejectsUnbalancedDrillingMoment) {
|
||||
const auto domain = PanelDomain(false, false, true);
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
const fesa::SerialParallelFor serial;
|
||||
fesa::MklPardisoSolver solver;
|
||||
const auto result =
|
||||
fesa::LinearStaticProcedureExecutor{serial, solver}.Execute(
|
||||
domain.Value());
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
EXPECT_NE(StatusText(result.GetStatus())
|
||||
.find("global-equilibrium-tolerance-failure"),
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST(LinearFrequencyProcedureExecutor,
|
||||
FreePanelRetainsSixRigidModesAndAllComputedEvidence) {
|
||||
auto domain = PanelDomain(false, true, false, 6U);
|
||||
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||
const auto result =
|
||||
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
|
||||
const auto& state = result.Value();
|
||||
EXPECT_EQ(state.mass_null_dimension, 4U);
|
||||
EXPECT_EQ(state.finite_dimension, 20U);
|
||||
ASSERT_EQ(state.modes.size(), 6U);
|
||||
EXPECT_GE(state.eigenpair_checks.size(), 6U);
|
||||
for (const auto& mode : state.modes) {
|
||||
EXPECT_EQ(mode.classification, 0U);
|
||||
EXPECT_EQ(mode.cluster_id, 1U);
|
||||
}
|
||||
EXPECT_LE(state.eta_gram, 1e-10);
|
||||
EXPECT_LE(state.max_eta_orth, 1e-10);
|
||||
EXPECT_LE(state.eta_mz, 1e-12);
|
||||
EXPECT_LE(state.eta_kz, 1e-12);
|
||||
for (const auto& check : state.eigenpair_checks) {
|
||||
EXPECT_LE(check.eta_r, 1e-10);
|
||||
EXPECT_LE(check.eta_norm, 1e-10);
|
||||
EXPECT_LE(check.eta_energy, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LinearFrequencyProcedureExecutor,
|
||||
ExcessRequestedModesFailsWithSourceContext) {
|
||||
auto domain = PanelDomain(false, false, false, 2U);
|
||||
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||
const auto result =
|
||||
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kModel);
|
||||
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
||||
EXPECT_EQ(result.GetStatus().Diagnostics()[0].code,
|
||||
"insufficient-finite-modes");
|
||||
EXPECT_GT(result.GetStatus().Diagnostics()[0].location.line, 0U);
|
||||
}
|
||||
|
||||
TEST(LinearFrequencyProcedureExecutor, PreservesDensityAndPhysicalUnitScaling) {
|
||||
const auto baseline_domain = PanelDomain(false, false);
|
||||
const auto dense_domain = PanelDomain(false, false, false, 1U, 1.0, 4.0);
|
||||
constexpr double length = 1024.0;
|
||||
const auto units_domain =
|
||||
PanelDomain(false, false, false, 1U, length,
|
||||
1.0 / (length * length * length), 12.0 / length);
|
||||
ASSERT_TRUE(baseline_domain.HasValue());
|
||||
ASSERT_TRUE(dense_domain.HasValue());
|
||||
ASSERT_TRUE(units_domain.HasValue());
|
||||
const fesa::LinearFrequencyProcedureExecutor executor;
|
||||
const auto baseline = executor.Execute(baseline_domain.Value());
|
||||
const auto dense = executor.Execute(dense_domain.Value());
|
||||
const auto units = executor.Execute(units_domain.Value());
|
||||
ASSERT_TRUE(baseline.HasValue()) << StatusText(baseline.GetStatus());
|
||||
ASSERT_TRUE(dense.HasValue()) << StatusText(dense.GetStatus());
|
||||
ASSERT_TRUE(units.HasValue()) << StatusText(units.GetStatus());
|
||||
EXPECT_NEAR(
|
||||
dense.Value().modes[0].eigenvalue / baseline.Value().modes[0].eigenvalue,
|
||||
0.25, 1e-12);
|
||||
EXPECT_NEAR(dense.Value().vectors(0U, 5U) / baseline.Value().vectors(0U, 5U),
|
||||
0.5, 1e-12);
|
||||
EXPECT_NEAR(
|
||||
units.Value().modes[0].eigenvalue / baseline.Value().modes[0].eigenvalue,
|
||||
1.0, 1e-12);
|
||||
EXPECT_NEAR(
|
||||
units.Value().vectors(0U, 5U) * length / baseline.Value().vectors(0U, 5U),
|
||||
1.0, 1e-12);
|
||||
EXPECT_LE(units.Value().eigenpair_checks[0].eta_r, 1e-10);
|
||||
EXPECT_LE(units.Value().eigenpair_checks[0].eta_energy, 1e-10);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <hdf5.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/app/fesa_application.h"
|
||||
|
||||
namespace {
|
||||
|
||||
class EigenApplicationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
static std::atomic<unsigned long long> sequence{0U};
|
||||
folder_ =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("fesa-eigen-app-" +
|
||||
std::to_string(
|
||||
std::chrono::steady_clock::now().time_since_epoch().count()) +
|
||||
"-" + std::to_string(sequence.fetch_add(1U)));
|
||||
ASSERT_TRUE(std::filesystem::create_directory(folder_));
|
||||
input_ = folder_ / "Panel.Modes.inp";
|
||||
output_ = folder_ / "Panel.Modes_results.h5";
|
||||
log_ = folder_ / "Panel.Modes_log.txt";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove_all(folder_, ignored);
|
||||
}
|
||||
|
||||
void WriteDeck(const bool empty = false) {
|
||||
std::ofstream file{input_};
|
||||
file << R"inp(*Part, name=Panel
|
||||
*Node
|
||||
11,0,0,0
|
||||
22,1,0,0
|
||||
33,1,1,0
|
||||
44,0,1,0
|
||||
*Element,type=S4
|
||||
71,11,22,33,44
|
||||
*Elset,elset=Shell
|
||||
71
|
||||
*Shell Section,elset=Shell,material=Steel
|
||||
0.5
|
||||
*End Part
|
||||
*Assembly,name=Assembly
|
||||
*Instance,name=Panel-1,part=Panel
|
||||
*End Instance
|
||||
*Nset,nset=Fixed,instance=Panel-1
|
||||
11,22,33
|
||||
*Nset,nset=Tip,instance=Panel-1
|
||||
44
|
||||
*End Assembly
|
||||
*Material,name=Steel
|
||||
*Density
|
||||
1.
|
||||
*Elastic
|
||||
12.,0.
|
||||
*Boundary
|
||||
Fixed,1,6
|
||||
Tip,1,2
|
||||
Tip,4,6
|
||||
)inp";
|
||||
if (empty) file << "Tip,3,3\n";
|
||||
file << R"inp(*Step,name=Step-2,nlgeom=NO,perturbation
|
||||
*Frequency,eigensolver=Lanczos,normalization=mass
|
||||
1
|
||||
*End Step
|
||||
)inp";
|
||||
ASSERT_TRUE(file.good());
|
||||
}
|
||||
|
||||
std::filesystem::path folder_, input_, output_, log_;
|
||||
};
|
||||
|
||||
TEST_F(EigenApplicationTest, DispatchesFrequencyAndWritesModalSibling) {
|
||||
WriteDeck();
|
||||
ASSERT_EQ(fesa::FesaApplication{}.Run({input_.string()}), 0);
|
||||
ASSERT_TRUE(std::filesystem::exists(output_));
|
||||
ASSERT_TRUE(std::filesystem::exists(log_));
|
||||
EXPECT_EQ(std::filesystem::file_size(log_), 0U);
|
||||
const hid_t file =
|
||||
H5Fopen(output_.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
||||
ASSERT_GE(file, 0);
|
||||
EXPECT_GT(H5Lexists(file, "/steps/Step-1/eigen/modes", H5P_DEFAULT), 0);
|
||||
EXPECT_GT(H5Lexists(file, "/steps/Step-1/eigen/vectors", H5P_DEFAULT), 0);
|
||||
EXPECT_EQ(H5Lexists(file, "/steps/Step-1/frames", H5P_DEFAULT), 0);
|
||||
EXPECT_GE(H5Fclose(file), 0);
|
||||
}
|
||||
|
||||
TEST_F(EigenApplicationTest, EmptyPencilPreservesPreviousFinalAndEmptyLog) {
|
||||
WriteDeck(true);
|
||||
const std::string original = "previous authoritative file";
|
||||
{
|
||||
std::ofstream previous{output_, std::ios::binary};
|
||||
previous << original;
|
||||
}
|
||||
EXPECT_EQ(fesa::FesaApplication{}.Run({input_.string()}), 4);
|
||||
std::ifstream previous{output_, std::ios::binary};
|
||||
EXPECT_TRUE(std::string(std::istreambuf_iterator<char>{previous}, {}) ==
|
||||
original);
|
||||
EXPECT_EQ(std::filesystem::file_size(log_), 0U);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,70 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "eigen_reference_comparison.h"
|
||||
#include "fesa/app/fesa_application.h"
|
||||
|
||||
#ifndef FESA_TEST_SOURCE_DIR
|
||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||
#endif
|
||||
|
||||
#ifndef FESA_TEST_BINARY_DIR
|
||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ReadBytes(const std::filesystem::path& path) {
|
||||
std::ifstream stream{path, std::ios::binary};
|
||||
if (!stream) {
|
||||
throw std::runtime_error{"Unable to read declared eigen artifact."};
|
||||
}
|
||||
return {std::istreambuf_iterator<char>{stream},
|
||||
std::istreambuf_iterator<char>{}};
|
||||
}
|
||||
|
||||
// E2-C25.
|
||||
TEST(EigenReference, RunsOriginalInputAndComparesAllModalFrameIdentities) {
|
||||
const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR};
|
||||
const std::filesystem::path reference_directory =
|
||||
source_root / "reference" / "shellNL-eigen-meshfine";
|
||||
const std::filesystem::path input =
|
||||
reference_directory / "shellNL-eigen-meshfine.inp";
|
||||
const std::filesystem::path text =
|
||||
reference_directory / "shellNL-eigen-meshfine eigenvalues.txt";
|
||||
const std::string input_before = ReadBytes(input);
|
||||
const std::string text_before = ReadBytes(text);
|
||||
|
||||
fesa::FesaApplication application;
|
||||
const int exit_code = application.Run({input.string()});
|
||||
EXPECT_EQ(ReadBytes(input), input_before);
|
||||
EXPECT_EQ(ReadBytes(text), text_before);
|
||||
ASSERT_EQ(exit_code, 0);
|
||||
const std::filesystem::path results =
|
||||
reference_directory / "shellNL-eigen-meshfine_results.h5";
|
||||
ASSERT_TRUE(std::filesystem::is_regular_file(results));
|
||||
const auto comparison = fesa::test::EigenReferenceComparison::Compare(
|
||||
{"shellnl-eigen-meshfine", input, text, results});
|
||||
ASSERT_TRUE(comparison.HasValue());
|
||||
EXPECT_TRUE(comparison.Value().passed);
|
||||
EXPECT_EQ(comparison.Value().source_step_name, "Step-2");
|
||||
EXPECT_EQ(comparison.Value().requested_mode_count, 10U);
|
||||
ASSERT_EQ(comparison.Value().rows.size(), 10U);
|
||||
ASSERT_EQ(comparison.Value().families.size(), 10U);
|
||||
for (std::size_t index = 0U; index < comparison.Value().rows.size();
|
||||
++index) {
|
||||
EXPECT_EQ(comparison.Value().rows[index].mode_id, index + 1U);
|
||||
EXPECT_EQ(comparison.Value().rows[index].component, "Value");
|
||||
EXPECT_EQ(comparison.Value().families[index].row_count, 1U);
|
||||
EXPECT_TRUE(comparison.Value().families[index].passed);
|
||||
}
|
||||
EXPECT_EQ(ReadBytes(input), input_before);
|
||||
EXPECT_EQ(ReadBytes(text), text_before);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,432 @@
|
||||
#include "eigen_reference_comparison.h"
|
||||
|
||||
#include <hdf5.h>
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/io/abaqus/domain_mapper.h"
|
||||
#include "fesa/io/abaqus/input_reader.h"
|
||||
|
||||
namespace fesa::test {
|
||||
namespace {
|
||||
|
||||
constexpr char kModesPath[] = "/steps/Step-1/eigen/modes";
|
||||
|
||||
class ComparisonFailure final : public std::runtime_error {
|
||||
public:
|
||||
ComparisonFailure(std::string code, std::string message)
|
||||
: std::runtime_error{std::move(message)}, code_{std::move(code)} {}
|
||||
|
||||
const std::string& Code() const noexcept { return code_; }
|
||||
|
||||
private:
|
||||
std::string code_;
|
||||
};
|
||||
|
||||
[[noreturn]] void Fail(const std::string& code, const std::string& message) {
|
||||
throw ComparisonFailure{code, message};
|
||||
}
|
||||
|
||||
Status FailureStatus(const EigenReferenceCase& reference_case,
|
||||
const std::string& code, const std::string& message) {
|
||||
return Status::Failure(FailureCategory::kModel, {{Severity::kError,
|
||||
code,
|
||||
{},
|
||||
"FREQUENCY",
|
||||
reference_case.case_id,
|
||||
message}});
|
||||
}
|
||||
|
||||
class Hdf5Handle {
|
||||
public:
|
||||
using Closer = herr_t (*)(hid_t);
|
||||
|
||||
Hdf5Handle() = default;
|
||||
Hdf5Handle(hid_t value, Closer closer) : value_{value}, closer_{closer} {}
|
||||
Hdf5Handle(const Hdf5Handle&) = delete;
|
||||
Hdf5Handle& operator=(const Hdf5Handle&) = delete;
|
||||
Hdf5Handle(Hdf5Handle&& other) noexcept
|
||||
: value_{other.value_}, closer_{other.closer_} {
|
||||
other.value_ = -1;
|
||||
}
|
||||
~Hdf5Handle() {
|
||||
if (value_ >= 0 && closer_ != nullptr) {
|
||||
(void)closer_(value_);
|
||||
}
|
||||
}
|
||||
|
||||
hid_t Get() const noexcept { return value_; }
|
||||
|
||||
private:
|
||||
hid_t value_{-1};
|
||||
Closer closer_{nullptr};
|
||||
};
|
||||
|
||||
hid_t RequireId(hid_t value, const char* message) {
|
||||
if (value < 0) {
|
||||
Fail("invalid-eigen-hdf5-schema", message);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void RequireHdf(herr_t value, const char* message) {
|
||||
if (value < 0) {
|
||||
Fail("invalid-eigen-hdf5-schema", message);
|
||||
}
|
||||
}
|
||||
|
||||
std::string ReadUtf8Attribute(hid_t object, const char* name) {
|
||||
Hdf5Handle attribute{RequireId(H5Aopen(object, name, H5P_DEFAULT),
|
||||
"A required HDF5 attribute is missing."),
|
||||
H5Aclose};
|
||||
Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()),
|
||||
"Unable to inspect an HDF5 attribute type."),
|
||||
H5Tclose};
|
||||
if (H5Tget_class(type.Get()) != H5T_STRING ||
|
||||
H5Tget_cset(type.Get()) != H5T_CSET_UTF8 ||
|
||||
H5Tis_variable_str(type.Get()) <= 0) {
|
||||
Fail("invalid-eigen-hdf5-schema",
|
||||
"A required HDF5 string attribute has the wrong type.");
|
||||
}
|
||||
Hdf5Handle space{RequireId(H5Aget_space(attribute.Get()),
|
||||
"Unable to inspect an HDF5 attribute shape."),
|
||||
H5Sclose};
|
||||
if (H5Sget_simple_extent_ndims(space.Get()) != 0) {
|
||||
Fail("invalid-eigen-hdf5-schema",
|
||||
"A required HDF5 string attribute is not scalar.");
|
||||
}
|
||||
char* value = nullptr;
|
||||
RequireHdf(H5Aread(attribute.Get(), type.Get(), &value),
|
||||
"Unable to read an HDF5 string attribute.");
|
||||
if (value == nullptr) {
|
||||
Fail("invalid-eigen-hdf5-schema", "An HDF5 string attribute is null.");
|
||||
}
|
||||
const std::string result{value};
|
||||
RequireHdf(H5free_memory(value), "Unable to release HDF5 string memory.");
|
||||
return result;
|
||||
}
|
||||
|
||||
std::uint64_t ReadUint64Attribute(hid_t object, const char* name) {
|
||||
Hdf5Handle attribute{RequireId(H5Aopen(object, name, H5P_DEFAULT),
|
||||
"A required HDF5 attribute is missing."),
|
||||
H5Aclose};
|
||||
Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()),
|
||||
"Unable to inspect an HDF5 attribute type."),
|
||||
H5Tclose};
|
||||
if (H5Tget_class(type.Get()) != H5T_INTEGER ||
|
||||
H5Tget_size(type.Get()) != sizeof(std::uint64_t) ||
|
||||
H5Tget_sign(type.Get()) != H5T_SGN_NONE) {
|
||||
Fail("invalid-eigen-hdf5-schema",
|
||||
"A required HDF5 integer attribute has the wrong type.");
|
||||
}
|
||||
Hdf5Handle space{RequireId(H5Aget_space(attribute.Get()),
|
||||
"Unable to inspect an HDF5 attribute shape."),
|
||||
H5Sclose};
|
||||
if (H5Sget_simple_extent_ndims(space.Get()) != 0) {
|
||||
Fail("invalid-eigen-hdf5-schema",
|
||||
"A required HDF5 integer attribute is not scalar.");
|
||||
}
|
||||
std::uint64_t value = 0U;
|
||||
RequireHdf(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value),
|
||||
"Unable to read an HDF5 integer attribute.");
|
||||
return value;
|
||||
}
|
||||
|
||||
struct Hdf5Mode {
|
||||
std::uint64_t mode_id;
|
||||
double eigenvalue;
|
||||
std::uint8_t classification;
|
||||
std::uint64_t cluster_id;
|
||||
};
|
||||
|
||||
std::vector<Hdf5Mode> ReadModes(hid_t file, std::uint64_t expected_count,
|
||||
const std::string& source_step_name) {
|
||||
Hdf5Handle dataset{RequireId(H5Dopen2(file, kModesPath, H5P_DEFAULT),
|
||||
"The eigen modes dataset is missing."),
|
||||
H5Dclose};
|
||||
Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()),
|
||||
"Unable to inspect the modes dataspace."),
|
||||
H5Sclose};
|
||||
if (H5Sget_simple_extent_ndims(space.Get()) != 1) {
|
||||
Fail("invalid-eigen-hdf5-schema", "The modes dataset must be rank one.");
|
||||
}
|
||||
hsize_t count = 0U;
|
||||
RequireHdf(H5Sget_simple_extent_dims(space.Get(), &count, nullptr),
|
||||
"Unable to inspect the modes dataset extent.");
|
||||
if (count != expected_count) {
|
||||
Fail("eigen-mode-identity-mismatch",
|
||||
"The modes dataset count does not equal requested mode count.");
|
||||
}
|
||||
Hdf5Handle type{RequireId(H5Dget_type(dataset.Get()),
|
||||
"Unable to inspect the modes dataset type."),
|
||||
H5Tclose};
|
||||
if (H5Tget_class(type.Get()) != H5T_COMPOUND ||
|
||||
H5Tget_nmembers(type.Get()) != 4 ||
|
||||
H5Tget_member_index(type.Get(), "mode_id") < 0 ||
|
||||
H5Tget_member_index(type.Get(), "eigenvalue") < 0 ||
|
||||
H5Tget_member_index(type.Get(), "classification") < 0 ||
|
||||
H5Tget_member_index(type.Get(), "cluster_id") < 0) {
|
||||
Fail("invalid-eigen-hdf5-schema", "The modes compound schema is invalid.");
|
||||
}
|
||||
const std::array<hid_t, 4> expected_types{H5T_STD_U64LE, H5T_IEEE_F64LE,
|
||||
H5T_STD_U8LE, H5T_STD_U64LE};
|
||||
const std::array<const char*, 4> names{"mode_id", "eigenvalue",
|
||||
"classification", "cluster_id"};
|
||||
for (std::size_t index = 0U; index < names.size(); ++index) {
|
||||
Hdf5Handle member{
|
||||
RequireId(H5Tget_member_type(type.Get(), static_cast<unsigned>(index)),
|
||||
"Unable to inspect a modes compound member."),
|
||||
H5Tclose};
|
||||
char* raw_name =
|
||||
H5Tget_member_name(type.Get(), static_cast<unsigned>(index));
|
||||
if (raw_name == nullptr) {
|
||||
Fail("invalid-eigen-hdf5-schema",
|
||||
"Unable to inspect a modes member name.");
|
||||
}
|
||||
const std::string member_name{raw_name};
|
||||
RequireHdf(H5free_memory(raw_name),
|
||||
"Unable to release a modes member name.");
|
||||
if (member_name != names[index] ||
|
||||
H5Tequal(member.Get(), expected_types[index]) <= 0) {
|
||||
Fail("invalid-eigen-hdf5-schema",
|
||||
"The modes compound member type or order is invalid.");
|
||||
}
|
||||
}
|
||||
Hdf5Handle memory_type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(Hdf5Mode)),
|
||||
"Unable to create a modes memory type."),
|
||||
H5Tclose};
|
||||
RequireHdf(H5Tinsert(memory_type.Get(), "mode_id", HOFFSET(Hdf5Mode, mode_id),
|
||||
H5T_NATIVE_UINT64),
|
||||
"Unable to define the mode identifier field.");
|
||||
RequireHdf(H5Tinsert(memory_type.Get(), "eigenvalue",
|
||||
HOFFSET(Hdf5Mode, eigenvalue), H5T_NATIVE_DOUBLE),
|
||||
"Unable to define the eigenvalue field.");
|
||||
RequireHdf(H5Tinsert(memory_type.Get(), "classification",
|
||||
HOFFSET(Hdf5Mode, classification), H5T_NATIVE_UINT8),
|
||||
"Unable to define the classification field.");
|
||||
RequireHdf(H5Tinsert(memory_type.Get(), "cluster_id",
|
||||
HOFFSET(Hdf5Mode, cluster_id), H5T_NATIVE_UINT64),
|
||||
"Unable to define the cluster field.");
|
||||
std::vector<Hdf5Mode> modes(static_cast<std::size_t>(count));
|
||||
RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL,
|
||||
H5P_DEFAULT, modes.data()),
|
||||
"Unable to read the modes dataset.");
|
||||
for (std::size_t index = 0U; index < modes.size(); ++index) {
|
||||
if (modes[index].mode_id != index + 1U ||
|
||||
modes[index].mode_id > static_cast<std::uint64_t>((
|
||||
std::numeric_limits<std::int64_t>::max)()) ||
|
||||
!std::isfinite(modes[index].eigenvalue)) {
|
||||
Fail("eigen-mode-identity-mismatch",
|
||||
"Mode IDs must be ordered one through requested mode count.");
|
||||
}
|
||||
}
|
||||
if (ReadUtf8Attribute(dataset.Get(), "quantity") != "eigenvalue" ||
|
||||
ReadUint64Attribute(dataset.Get(), "eigen_schema_version") != 1U ||
|
||||
ReadUtf8Attribute(dataset.Get(), "source_step_name") !=
|
||||
source_step_name ||
|
||||
ReadUtf8Attribute(dataset.Get(), "domain") != "mode" ||
|
||||
ReadUtf8Attribute(dataset.Get(), "component_names") != "Value" ||
|
||||
ReadUtf8Attribute(dataset.Get(), "component_unit_dimensions") !=
|
||||
"time^-2" ||
|
||||
ReadUtf8Attribute(dataset.Get(), "coordinate_system") != "scalar" ||
|
||||
ReadUtf8Attribute(dataset.Get(), "location") != "modal-frame") {
|
||||
Fail("invalid-eigen-hdf5-schema", "Modes dataset metadata is invalid.");
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
double ParseFiniteNumber(const std::string& token) {
|
||||
static const std::regex number{
|
||||
R"([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)"};
|
||||
if (!std::regex_match(token, number)) {
|
||||
Fail("invalid-eigen-reference-text", "A TXT numeric token is malformed.");
|
||||
}
|
||||
double value = 0.0;
|
||||
const char* first = token.data() + (token.front() == '+' ? 1U : 0U);
|
||||
const auto conversion =
|
||||
std::from_chars(first, token.data() + token.size(), value);
|
||||
if (conversion.ec != std::errc{} ||
|
||||
conversion.ptr != token.data() + token.size() || !std::isfinite(value)) {
|
||||
Fail("invalid-eigen-reference-text", "A TXT Value is nonfinite.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void RequireNumberSyntax(const std::string& token) {
|
||||
static const std::regex number{
|
||||
R"([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)"};
|
||||
if (!std::regex_match(token, number)) {
|
||||
Fail("invalid-eigen-reference-text", "A TXT numeric token is malformed.");
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::uint64_t, double> ReadReferenceText(
|
||||
const std::filesystem::path& path) {
|
||||
std::ifstream stream{path, std::ios::binary};
|
||||
if (!stream) {
|
||||
Fail("needs-reference-artifacts",
|
||||
"The eigenvalue TXT artifact is missing.");
|
||||
}
|
||||
const std::regex row{
|
||||
R"(^[ \t]*Frame:[ \t]+Mode[ \t]+([0-9]+):[ \t]+Value[ \t]*=[ \t]*([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)[ \t]+Freq[ \t]*=[ \t]*([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)[ \t]+\(cycles/time\)[ \t]*$)"};
|
||||
std::map<std::uint64_t, double> rows;
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
if (line.find_first_not_of(" \t") == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
std::smatch match;
|
||||
if (!std::regex_match(line, match, row)) {
|
||||
Fail("invalid-eigen-reference-text",
|
||||
"A TXT row is not exact Frame/Mode syntax.");
|
||||
}
|
||||
std::uint64_t mode_id = 0U;
|
||||
const std::string mode_text = match[1].str();
|
||||
const auto parsed = std::from_chars(
|
||||
mode_text.data(), mode_text.data() + mode_text.size(), mode_id);
|
||||
if (parsed.ec != std::errc{} ||
|
||||
parsed.ptr != mode_text.data() + mode_text.size() || mode_id == 0U ||
|
||||
mode_id > static_cast<std::uint64_t>(
|
||||
(std::numeric_limits<std::int64_t>::max)()) ||
|
||||
!rows.emplace(mode_id, ParseFiniteNumber(match[2].str())).second) {
|
||||
Fail("eigen-mode-identity-mismatch",
|
||||
"TXT mode IDs must be unique positive integers.");
|
||||
}
|
||||
RequireNumberSyntax(match[3].str());
|
||||
}
|
||||
if (rows.empty()) {
|
||||
Fail("invalid-eigen-reference-text",
|
||||
"The eigenvalue TXT artifact is empty.");
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result<EigenReferenceComparisonReport> EigenReferenceComparison::Compare(
|
||||
const EigenReferenceCase& reference_case) {
|
||||
try {
|
||||
auto parsed = AbaqusInputReader{}.Read(reference_case.input_path);
|
||||
if (!parsed.HasValue()) {
|
||||
Fail("needs-reference-artifacts", "The eigen input cannot be parsed.");
|
||||
}
|
||||
auto domain = AbaqusDomainMapper{}.Map(parsed.Value());
|
||||
if (!domain.HasValue() || domain.Value().Frequency() == nullptr) {
|
||||
Fail("needs-reference-artifacts",
|
||||
"The input has no sole frequency step.");
|
||||
}
|
||||
const FrequencyStepDefinition& frequency = *domain.Value().Frequency();
|
||||
const std::uint64_t requested =
|
||||
static_cast<std::uint64_t>(frequency.requested_mode_count);
|
||||
const auto reference =
|
||||
ReadReferenceText(reference_case.eigenvalues_text_path);
|
||||
Hdf5Handle file{
|
||||
RequireId(H5Fopen(reference_case.results_hdf5_path.string().c_str(),
|
||||
H5F_ACC_RDONLY, H5P_DEFAULT),
|
||||
"The eigen HDF5 result is missing."),
|
||||
H5Fclose};
|
||||
Hdf5Handle metadata{
|
||||
RequireId(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT),
|
||||
"The metadata group is missing."),
|
||||
H5Gclose};
|
||||
if (ReadUint64Attribute(metadata.Get(), "schema_version") != 0U ||
|
||||
ReadUint64Attribute(metadata.Get(), "eigen_schema_version") != 1U ||
|
||||
ReadUtf8Attribute(metadata.Get(), "feature_id") !=
|
||||
"mitc4-shell-feast-eigen2" ||
|
||||
ReadUtf8Attribute(metadata.Get(), "analysis_procedure") !=
|
||||
"linear-frequency" ||
|
||||
ReadUtf8Attribute(metadata.Get(), "mass_variant") != "derived-lumped" ||
|
||||
ReadUtf8Attribute(metadata.Get(), "normalization") != "mass" ||
|
||||
ReadUtf8Attribute(metadata.Get(), "eigensolver_backend") !=
|
||||
"MKL FEAST") {
|
||||
Fail("invalid-eigen-hdf5-schema",
|
||||
"The eigen result discriminator is invalid.");
|
||||
}
|
||||
const std::string expected_identity =
|
||||
"path=" + parsed.Value().source_path.generic_string() +
|
||||
";content_identity=" + parsed.Value().source_content_identity;
|
||||
if (ReadUtf8Attribute(metadata.Get(), "source_input_identity") !=
|
||||
expected_identity) {
|
||||
Fail("eigen-source-identity-mismatch",
|
||||
"The HDF5 source input identity differs from the executed input.");
|
||||
}
|
||||
Hdf5Handle step{
|
||||
RequireId(H5Gopen2(file.Get(), "/steps/Step-1", H5P_DEFAULT),
|
||||
"The eigen storage step is missing."),
|
||||
H5Gclose};
|
||||
if (ReadUtf8Attribute(step.Get(), "source_step_name") !=
|
||||
frequency.source_step_name_raw ||
|
||||
ReadUint64Attribute(step.Get(), "requested_mode_count") != requested ||
|
||||
ReadUint64Attribute(step.Get(), "returned_mode_count") != requested) {
|
||||
Fail("eigen-mode-identity-mismatch",
|
||||
"The HDF5 source step or requested mode count is inconsistent.");
|
||||
}
|
||||
const auto modes =
|
||||
ReadModes(file.Get(), requested, frequency.source_step_name_raw);
|
||||
if (reference.size() != modes.size()) {
|
||||
Fail("eigen-mode-identity-mismatch",
|
||||
"TXT and HDF5 mode inventories have different sizes.");
|
||||
}
|
||||
|
||||
EigenReferenceComparisonReport report{};
|
||||
report.case_id = reference_case.case_id;
|
||||
report.source_step_name = frequency.source_step_name_raw;
|
||||
report.requested_mode_count = requested;
|
||||
std::vector<ReferenceToleranceFamily> families;
|
||||
families.reserve(modes.size());
|
||||
for (const auto& mode : modes) {
|
||||
const auto row = reference.find(mode.mode_id);
|
||||
if (row == reference.end()) {
|
||||
Fail("eigen-mode-identity-mismatch",
|
||||
"A HDF5 mode is missing from TXT.");
|
||||
}
|
||||
families.push_back({"case=" + reference_case.case_id +
|
||||
";source_step=" + frequency.source_step_name_raw +
|
||||
";mode_id=" + std::to_string(mode.mode_id) +
|
||||
";component=Value",
|
||||
{"Value"},
|
||||
true,
|
||||
{{mode.eigenvalue, row->second}}});
|
||||
}
|
||||
const auto evaluation = ReferenceTolerancePolicy::Evaluate(families);
|
||||
if (!evaluation.valid || evaluation.families.size() != families.size()) {
|
||||
Fail("nonfinite-comparison-value",
|
||||
"Tolerance evaluation rejected modal values.");
|
||||
}
|
||||
report.families = evaluation.families;
|
||||
report.passed = evaluation.passed;
|
||||
for (std::size_t index = 0U; index < modes.size(); ++index) {
|
||||
const auto& policy = report.families[index];
|
||||
report.rows.push_back(
|
||||
{frequency.source_step_name_raw, modes[index].mode_id, "Value",
|
||||
modes[index].eigenvalue, reference.at(modes[index].mode_id),
|
||||
policy.rows[0U]});
|
||||
}
|
||||
return Result<EigenReferenceComparisonReport>::Success(std::move(report));
|
||||
} catch (const ComparisonFailure& exception) {
|
||||
return Result<EigenReferenceComparisonReport>::Failure(
|
||||
FailureStatus(reference_case, exception.Code(), exception.what()));
|
||||
} catch (const std::exception& exception) {
|
||||
return Result<EigenReferenceComparisonReport>::Failure(
|
||||
FailureStatus(reference_case, "eigen-reference-comparison-failure",
|
||||
exception.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fesa::test
|
||||
@@ -0,0 +1,49 @@
|
||||
#ifndef FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
|
||||
#define FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "reference_tolerance_policy.h"
|
||||
|
||||
namespace fesa::test {
|
||||
|
||||
struct EigenReferenceCase {
|
||||
std::string case_id;
|
||||
std::filesystem::path input_path;
|
||||
std::filesystem::path eigenvalues_text_path;
|
||||
std::filesystem::path results_hdf5_path;
|
||||
};
|
||||
|
||||
struct EigenReferenceRowDecision {
|
||||
std::string source_step_name;
|
||||
std::uint64_t mode_id;
|
||||
std::string component;
|
||||
double fesa_value;
|
||||
double reference_value;
|
||||
ReferenceToleranceRowDecision tolerance;
|
||||
};
|
||||
|
||||
struct EigenReferenceComparisonReport {
|
||||
std::string case_id;
|
||||
std::string source_step_name;
|
||||
std::uint64_t requested_mode_count;
|
||||
std::vector<EigenReferenceRowDecision> rows;
|
||||
std::vector<ReferenceToleranceFamilyDecision> families;
|
||||
bool passed;
|
||||
};
|
||||
|
||||
/// @brief Compares modal eigenvalues without changing the approved input or
|
||||
/// TXT.
|
||||
class EigenReferenceComparison {
|
||||
public:
|
||||
static Result<EigenReferenceComparisonReport> Compare(
|
||||
const EigenReferenceCase& reference_case);
|
||||
};
|
||||
|
||||
} // namespace fesa::test
|
||||
|
||||
#endif // FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "eigen_reference_comparison.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <hdf5.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/io/abaqus/input_reader.h"
|
||||
|
||||
// The direct fixture is intentionally kept in this test file: it exercises the
|
||||
// comparator contract without depending on the application or HDF5 writer.
|
||||
namespace {
|
||||
std::filesystem::path UniqueFixtureDirectory() {
|
||||
static std::atomic<std::uint64_t> serial{0U};
|
||||
const auto ticks =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
return std::filesystem::temp_directory_path() /
|
||||
("fesa-eigen-direct-" + std::to_string(ticks) + "-" +
|
||||
std::to_string(serial.fetch_add(1U)));
|
||||
}
|
||||
|
||||
struct Mode {
|
||||
std::uint64_t mode_id;
|
||||
double eigenvalue;
|
||||
std::uint8_t classification;
|
||||
std::uint64_t cluster_id;
|
||||
};
|
||||
void Str(hid_t o, const char* n, const std::string& v) {
|
||||
hid_t t = H5Tcopy(H5T_C_S1);
|
||||
H5Tset_size(t, H5T_VARIABLE);
|
||||
H5Tset_cset(t, H5T_CSET_UTF8);
|
||||
hid_t s = H5Screate(H5S_SCALAR),
|
||||
a = H5Acreate2(o, n, t, s, H5P_DEFAULT, H5P_DEFAULT);
|
||||
const char* p = v.c_str();
|
||||
H5Awrite(a, t, &p);
|
||||
H5Aclose(a);
|
||||
H5Sclose(s);
|
||||
H5Tclose(t);
|
||||
}
|
||||
void U(hid_t o, const char* n, std::uint64_t v) {
|
||||
hid_t s = H5Screate(H5S_SCALAR),
|
||||
a = H5Acreate2(o, n, H5T_STD_U64LE, s, H5P_DEFAULT, H5P_DEFAULT);
|
||||
H5Awrite(a, H5T_NATIVE_UINT64, &v);
|
||||
H5Aclose(a);
|
||||
H5Sclose(s);
|
||||
}
|
||||
TEST(EigenReferenceComparison,
|
||||
DirectSchemaFixtureAcceptsRepeatedReorderedModes) {
|
||||
const auto d = UniqueFixtureDirectory();
|
||||
std::error_code e;
|
||||
std::filesystem::create_directories(d, e);
|
||||
const auto inp = d / "x.inp", txt = d / "x.txt", h5 = d / "x.h5";
|
||||
std::ofstream(inp) << "*Part,name=P\n*Node\n1,0,0,0\n2,1,0,0\n3,1,1,0\n4,0,1,"
|
||||
"0\n*Element,type=S4\n1,1,2,3,4\n*Elset,elset=E\n1\n*"
|
||||
"Shell Section,elset=E,material=M\n1.\n*End "
|
||||
"Part\n*Assembly,name=A\n*Instance,name=I,part=P\n*End "
|
||||
"Instance\n*Nset,nset=R,instance=I\n1\n*End "
|
||||
"Assembly\n*Material,name=M\n*Density\n1.\n*Elastic\n1."
|
||||
",0.3\n*Boundary\nR,1,6\n*Step,name=Step-2,nlgeom=NO\n*"
|
||||
"Frequency\n2\n*End Step\n";
|
||||
std::ofstream(txt)
|
||||
<< "Frame: Mode 2: Value = 100 Freq = 1e999999 (cycles/time)\nFrame: "
|
||||
"Mode 1: Value = 100 Freq = 1 (cycles/time)\n";
|
||||
auto p = fesa::AbaqusInputReader{}.Read(inp);
|
||||
ASSERT_TRUE(p.HasValue());
|
||||
hid_t f = H5Fcreate(h5.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT,
|
||||
H5P_DEFAULT),
|
||||
m = H5Gcreate2(f, "/metadata", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
||||
U(m, "schema_version", 0);
|
||||
U(m, "eigen_schema_version", 1);
|
||||
Str(m, "feature_id", "mitc4-shell-feast-eigen2");
|
||||
Str(m, "analysis_procedure", "linear-frequency");
|
||||
Str(m, "mass_variant", "derived-lumped");
|
||||
Str(m, "normalization", "mass");
|
||||
Str(m, "eigensolver_backend", "MKL FEAST");
|
||||
Str(m, "source_input_identity",
|
||||
"path=" + p.Value().source_path.generic_string() +
|
||||
";content_identity=" + p.Value().source_content_identity);
|
||||
H5Gclose(m);
|
||||
hid_t ss = H5Gcreate2(f, "/steps", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
||||
s = H5Gcreate2(ss, "Step-1", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
||||
Str(s, "source_step_name", "Step-2");
|
||||
U(s, "requested_mode_count", 2);
|
||||
U(s, "returned_mode_count", 2);
|
||||
hid_t g = H5Gcreate2(s, "eigen", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
||||
hsize_t n = 2;
|
||||
hid_t sp = H5Screate_simple(1, &n, nullptr),
|
||||
t = H5Tcreate(H5T_COMPOUND, sizeof(Mode));
|
||||
H5Tinsert(t, "mode_id", HOFFSET(Mode, mode_id), H5T_STD_U64LE);
|
||||
H5Tinsert(t, "eigenvalue", HOFFSET(Mode, eigenvalue), H5T_IEEE_F64LE);
|
||||
H5Tinsert(t, "classification", HOFFSET(Mode, classification), H5T_STD_U8LE);
|
||||
H5Tinsert(t, "cluster_id", HOFFSET(Mode, cluster_id), H5T_STD_U64LE);
|
||||
hid_t ds =
|
||||
H5Dcreate2(g, "modes", t, sp, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
||||
Mode v[] = {{1, 100, 1, 1}, {2, 100, 1, 1}};
|
||||
H5Dwrite(ds, t, H5S_ALL, H5S_ALL, H5P_DEFAULT, v);
|
||||
U(ds, "eigen_schema_version", 1);
|
||||
Str(ds, "source_step_name", "Step-2");
|
||||
Str(ds, "domain", "mode");
|
||||
Str(ds, "quantity", "eigenvalue");
|
||||
Str(ds, "component_names", "Value");
|
||||
Str(ds, "component_unit_dimensions", "time^-2");
|
||||
Str(ds, "coordinate_system", "scalar");
|
||||
Str(ds, "location", "modal-frame");
|
||||
H5Dclose(ds);
|
||||
H5Tclose(t);
|
||||
H5Sclose(sp);
|
||||
H5Gclose(g);
|
||||
H5Gclose(s);
|
||||
H5Gclose(ss);
|
||||
H5Fclose(f);
|
||||
auto reference_case = fesa::test::EigenReferenceCase{"x", inp, txt, h5};
|
||||
auto ok = fesa::test::EigenReferenceComparison::Compare(reference_case);
|
||||
ASSERT_TRUE(ok.HasValue());
|
||||
EXPECT_EQ(ok.Value().families.size(), 2U);
|
||||
struct TextMutation {
|
||||
std::string text;
|
||||
std::string expected_code;
|
||||
};
|
||||
const std::vector<TextMutation> invalid_texts{
|
||||
{"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n",
|
||||
"eigen-mode-identity-mismatch"},
|
||||
{"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n"
|
||||
"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n",
|
||||
"eigen-mode-identity-mismatch"},
|
||||
{"Frame: Mode 1: Value = nan Freq = 1 (cycles/time)\n"
|
||||
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
|
||||
"invalid-eigen-reference-text"},
|
||||
{"Frame: Mode 184467440737095516160: Value = 100 Freq = 1 (cycles/time)\n"
|
||||
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
|
||||
"eigen-mode-identity-mismatch"},
|
||||
{"Frame: Mode -1: Value = 100 Freq = 1 (cycles/time)\n"
|
||||
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
|
||||
"invalid-eigen-reference-text"},
|
||||
{"Frame: Mode 3: Value = 100 Freq = 1 (cycles/time)\n"
|
||||
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
|
||||
"eigen-mode-identity-mismatch"}};
|
||||
for (const TextMutation& invalid_text : invalid_texts) {
|
||||
std::ofstream(txt, std::ios::trunc) << invalid_text.text;
|
||||
const auto comparison =
|
||||
fesa::test::EigenReferenceComparison::Compare(reference_case);
|
||||
ASSERT_FALSE(comparison.HasValue());
|
||||
ASSERT_EQ(comparison.GetStatus().Diagnostics().size(), 1U);
|
||||
EXPECT_EQ(comparison.GetStatus().Diagnostics()[0U].code,
|
||||
invalid_text.expected_code);
|
||||
}
|
||||
std::ofstream(txt, std::ios::trunc)
|
||||
<< "Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\nFrame: Mode 2: "
|
||||
"Value = 100 Freq = 1 (cycles/time)\n";
|
||||
f = H5Fopen(h5.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||
m = H5Gopen2(f, "/metadata", H5P_DEFAULT);
|
||||
H5Adelete(m, "source_input_identity");
|
||||
Str(m, "source_input_identity", "wrong");
|
||||
H5Gclose(m);
|
||||
H5Fclose(f);
|
||||
EXPECT_FALSE(
|
||||
fesa::test::EigenReferenceComparison::Compare(reference_case).HasValue());
|
||||
std::filesystem::remove_all(d, e);
|
||||
}
|
||||
} // namespace
|
||||
@@ -1,5 +1,3 @@
|
||||
#include "nonlinear_static_reference_comparison.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -13,6 +11,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "nonlinear_static_reference_comparison.h"
|
||||
|
||||
#ifndef FESA_TEST_SOURCE_DIR
|
||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||
#endif
|
||||
@@ -34,13 +34,10 @@ std::string ReadBytes(const std::filesystem::path& path) {
|
||||
|
||||
fesa::test::NonlinearReferenceCase MakeCase(const std::string& case_id) {
|
||||
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
||||
const bool fine = case_id == "shellnl-meshfine-s4-final";
|
||||
const bool nonlinear = case_id == "shellnl-s4-final" || fine;
|
||||
const bool nonlinear = case_id == "shellnl-s4-final";
|
||||
const std::filesystem::path directory =
|
||||
root / "reference" / (fine ? "shellNL-meshfine"
|
||||
: (nonlinear ? "shellNL" : "shell"));
|
||||
const std::string stem = fine ? "shellNL-meshfine"
|
||||
: (nonlinear ? "shellNL" : "shell");
|
||||
root / "reference" / (nonlinear ? "shellNL" : "shell");
|
||||
const std::string stem = nonlinear ? "shellNL" : "shell";
|
||||
return {case_id,
|
||||
directory / (stem + ".inp"),
|
||||
directory / (stem + " displacements.csv"),
|
||||
@@ -105,9 +102,8 @@ void ExpectInternalFamiliesPassed(
|
||||
<< family.family_identity << " rows=" << family.row_count
|
||||
<< " numerator=" << family.numerator << " scale=" << family.scale
|
||||
<< " metric="
|
||||
<< (family.normalized_metric.has_value()
|
||||
? *family.normalized_metric
|
||||
: -1.0)
|
||||
<< (family.normalized_metric.has_value() ? *family.normalized_metric
|
||||
: -1.0)
|
||||
<< " diagnostic=" << family.diagnostic_code;
|
||||
}
|
||||
}
|
||||
@@ -159,14 +155,14 @@ TEST(NonlinearStaticReference,
|
||||
EXPECT_EQ(generated.Value().copied_input_path, copied.Value());
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(
|
||||
generated.Value().generated_results_hdf5));
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(
|
||||
generated.Value().generated_log_path));
|
||||
EXPECT_TRUE(
|
||||
std::filesystem::is_regular_file(generated.Value().generated_log_path));
|
||||
EXPECT_EQ(generated.Value().generated_results_hdf5.filename(),
|
||||
case_id == "shellnl-s4-final" ? "shellNL_results.h5"
|
||||
: "shell_results.h5");
|
||||
EXPECT_EQ(generated.Value().generated_log_path.filename(),
|
||||
case_id == "shellnl-s4-final" ? "shellNL_log.txt"
|
||||
: "shell_log.txt");
|
||||
: "shell_results.h5");
|
||||
EXPECT_EQ(
|
||||
generated.Value().generated_log_path.filename(),
|
||||
case_id == "shellnl-s4-final" ? "shellNL_log.txt" : "shell_log.txt");
|
||||
|
||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
||||
reference_case, generated.Value().generated_results_hdf5);
|
||||
@@ -182,13 +178,12 @@ TEST(NonlinearStaticReference,
|
||||
|
||||
ExpectFamily(comparison.Value(),
|
||||
case_id == "shellnl-s4-final" ? "coarse.translation"
|
||||
: "linear.translation",
|
||||
: "linear.translation",
|
||||
147U, fesa::test::NonlinearReferenceDisposition::kBlocking);
|
||||
ExpectFamily(comparison.Value(),
|
||||
case_id == "shellnl-s4-final" ? "coarse.rotation"
|
||||
: "linear.rotation",
|
||||
147U,
|
||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly);
|
||||
ExpectFamily(
|
||||
comparison.Value(),
|
||||
case_id == "shellnl-s4-final" ? "coarse.rotation" : "linear.rotation",
|
||||
147U, fesa::test::NonlinearReferenceDisposition::kWarningOnly);
|
||||
if (case_id == "shellnl-s4-final") {
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_inplane_force",
|
||||
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
@@ -219,9 +214,9 @@ TEST(NonlinearStaticReference,
|
||||
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
|
||||
stream << "not an HDF5 file";
|
||||
stream.close();
|
||||
EXPECT_FALSE(fesa::test::CompareHdf5ToReferenceCsv(reference_case,
|
||||
invalid_hdf5)
|
||||
.HasValue());
|
||||
EXPECT_FALSE(
|
||||
fesa::test::CompareHdf5ToReferenceCsv(reference_case, invalid_hdf5)
|
||||
.HasValue());
|
||||
|
||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||
report.blocking_passed = true;
|
||||
@@ -230,17 +225,29 @@ TEST(NonlinearStaticReference,
|
||||
{"UR1", "UR2", "UR3"},
|
||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||
147U,
|
||||
{"nonlinear.rotation", {"UR1", "UR2", "UR3"}, false, 1.0, 0.01,
|
||||
0U, 0.02, 0.02, 0U, false, false, "relative-rms-exceeded", {}, 147U}});
|
||||
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
|
||||
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
|
||||
{"nonlinear.rotation",
|
||||
{"UR1", "UR2", "UR3"},
|
||||
false,
|
||||
1.0,
|
||||
0.01,
|
||||
0U,
|
||||
0.02,
|
||||
0.02,
|
||||
0U,
|
||||
false,
|
||||
false,
|
||||
"relative-rms-exceeded",
|
||||
{},
|
||||
147U}});
|
||||
EXPECT_EQ(
|
||||
fesa::test::ClassifyReferenceComparison(report),
|
||||
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
|
||||
}
|
||||
|
||||
// NSNR-REF-020.
|
||||
TEST(NonlinearStaticReference, AuditsFinePrimaryAndCoarseCompanionArtifacts) {
|
||||
const std::array<std::string, 3> case_ids{
|
||||
"shellnl-meshfine-s4-final", "shellnl-s4-final",
|
||||
"shell-s4-linear-limit"};
|
||||
TEST(NonlinearStaticReference, AuditsRetainedStaticArtifacts) {
|
||||
const std::array<std::string, 2> case_ids{"shellnl-s4-final",
|
||||
"shell-s4-linear-limit"};
|
||||
for (const auto& case_id : case_ids) {
|
||||
const auto reference_case = MakeCase(case_id);
|
||||
const auto before = fesa::test::ArtifactCheck(reference_case);
|
||||
@@ -281,16 +288,14 @@ TEST(NonlinearStaticReference,
|
||||
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_transverse_force",
|
||||
24U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment",
|
||||
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment", 48U,
|
||||
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
EXPECT_TRUE(comparison.Value().external_blocking_passed);
|
||||
}
|
||||
|
||||
// NSNR-REF-022.
|
||||
TEST(NonlinearStaticReference,
|
||||
BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
|
||||
for (const std::string case_id :
|
||||
{"shellnl-meshfine-s4-final", "shellnl-s4-final"}) {
|
||||
TEST(NonlinearStaticReference, BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
|
||||
for (const std::string case_id : {"shellnl-s4-final"}) {
|
||||
const auto reference_case = MakeCase(case_id);
|
||||
const auto copied =
|
||||
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
@@ -302,9 +307,7 @@ TEST(NonlinearStaticReference,
|
||||
reference_case, generated.Value().generated_results_hdf5);
|
||||
ASSERT_TRUE(comparison.HasValue()) << case_id;
|
||||
|
||||
const std::string prefix = case_id == "shellnl-meshfine-s4-final"
|
||||
? "fine."
|
||||
: "coarse.";
|
||||
const std::string prefix = "coarse.";
|
||||
const std::array<std::string, 4> identities{
|
||||
prefix + "global_force_balance", prefix + "global_moment_balance",
|
||||
prefix + "accepted_free_force_residual",
|
||||
@@ -313,7 +316,8 @@ TEST(NonlinearStaticReference,
|
||||
const auto found = std::find_if(
|
||||
comparison.Value().internal_equilibrium.begin(),
|
||||
comparison.Value().internal_equilibrium.end(),
|
||||
[&identity](const fesa::test::NonlinearInternalEquilibriumReport& row) {
|
||||
[&identity](
|
||||
const fesa::test::NonlinearInternalEquilibriumReport& row) {
|
||||
return row.family_identity == identity;
|
||||
});
|
||||
ASSERT_NE(found, comparison.Value().internal_equilibrium.end())
|
||||
@@ -321,9 +325,8 @@ TEST(NonlinearStaticReference,
|
||||
EXPECT_TRUE(found->passed)
|
||||
<< identity << " numerator=" << found->numerator
|
||||
<< " scale=" << found->scale << " metric="
|
||||
<< (found->normalized_metric.has_value()
|
||||
? *found->normalized_metric
|
||||
: -1.0)
|
||||
<< (found->normalized_metric.has_value() ? *found->normalized_metric
|
||||
: -1.0)
|
||||
<< " diagnostic=" << found->diagnostic_code;
|
||||
}
|
||||
EXPECT_TRUE(comparison.Value().internal_blocking_passed);
|
||||
@@ -334,19 +337,26 @@ TEST(NonlinearStaticReference,
|
||||
}
|
||||
|
||||
// NSNR-REF-023.
|
||||
TEST(NonlinearStaticReference,
|
||||
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
|
||||
TEST(
|
||||
NonlinearStaticReference,
|
||||
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
|
||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||
report.blocking_passed = true;
|
||||
report.external_blocking_passed = true;
|
||||
report.internal_blocking_passed = true;
|
||||
report.precheck_findings = {
|
||||
{"blocking", fesa::test::NonlinearReferenceDisposition::kBlocking,
|
||||
"missing-row", {}},
|
||||
{"warning", fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||
"duplicate-row", {}},
|
||||
{"review", fesa::test::NonlinearReferenceDisposition::kReviewOnly,
|
||||
"nonfinite-row", {}}};
|
||||
{"blocking",
|
||||
fesa::test::NonlinearReferenceDisposition::kBlocking,
|
||||
"missing-row",
|
||||
{}},
|
||||
{"warning",
|
||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||
"duplicate-row",
|
||||
{}},
|
||||
{"review",
|
||||
fesa::test::NonlinearReferenceDisposition::kReviewOnly,
|
||||
"nonfinite-row",
|
||||
{}}};
|
||||
|
||||
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
|
||||
fesa::test::NonlinearReferenceClassification::kFailed);
|
||||
@@ -359,22 +369,22 @@ TEST(NonlinearStaticReference,
|
||||
GlobalForceAndMomentBalanceUseApprovedInclusiveOneEighthGate) {
|
||||
for (const std::string& identity :
|
||||
{"fine.global_force_balance", "fine.global_moment_balance"}) {
|
||||
const auto below = fesa::test::EvaluateGlobalBalance(
|
||||
identity, 5.0e-9, 1.0, 0.0);
|
||||
const auto below =
|
||||
fesa::test::EvaluateGlobalBalance(identity, 5.0e-9, 1.0, 0.0);
|
||||
EXPECT_TRUE(below.passed) << identity;
|
||||
ASSERT_TRUE(below.normalized_metric.has_value()) << identity;
|
||||
EXPECT_EQ(*below.normalized_metric, 5.0e-9) << identity;
|
||||
|
||||
const auto boundary = fesa::test::EvaluateGlobalBalance(
|
||||
identity, 1.0e-8, 1.0, 0.0);
|
||||
const auto boundary =
|
||||
fesa::test::EvaluateGlobalBalance(identity, 1.0e-8, 1.0, 0.0);
|
||||
EXPECT_TRUE(boundary.passed) << identity;
|
||||
ASSERT_TRUE(boundary.normalized_metric.has_value()) << identity;
|
||||
EXPECT_EQ(*boundary.normalized_metric, 1.0e-8) << identity;
|
||||
|
||||
const auto above = fesa::test::EvaluateGlobalBalance(
|
||||
identity, std::nextafter(1.0e-8,
|
||||
std::numeric_limits<double>::infinity()),
|
||||
1.0, 0.0);
|
||||
identity,
|
||||
std::nextafter(1.0e-8, std::numeric_limits<double>::infinity()), 1.0,
|
||||
0.0);
|
||||
EXPECT_FALSE(above.passed) << identity;
|
||||
ASSERT_TRUE(above.normalized_metric.has_value()) << identity;
|
||||
EXPECT_GT(*above.normalized_metric, 1.0e-8) << identity;
|
||||
|
||||
@@ -1,198 +1,15 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "nonlinear_static_reference_comparison.h"
|
||||
|
||||
#ifndef FESA_TEST_SOURCE_DIR
|
||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||
#endif
|
||||
|
||||
#ifndef FESA_TEST_BINARY_DIR
|
||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
fesa::test::NonlinearReferenceCase MakePressureCase() {
|
||||
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
||||
const std::filesystem::path directory =
|
||||
root / "reference" / "shellNL-meshfine-pressure";
|
||||
return {"shellnl-meshfine-pressure-s4-follower",
|
||||
directory / "shellNL-meshfine-pressure.inp",
|
||||
directory / "shellNL-meshfine-pressure displacements.csv",
|
||||
directory / "shellNL-meshfine-pressure reactions.csv",
|
||||
directory / "shellNL-meshfine-pressure stresses.csv",
|
||||
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" /
|
||||
"shell-surface-pressure-load",
|
||||
fesa::test::NonlinearReferenceArtifactDisposition::kRequired,
|
||||
fesa::test::NonlinearReferenceArtifactDisposition::kRequired,
|
||||
fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly,
|
||||
fesa::test::NonlinearReferenceArtifactDisposition::kExcluded};
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> FindCachedResults(
|
||||
const fesa::test::NonlinearReferenceCase& reference_case) {
|
||||
const std::filesystem::path solver_library =
|
||||
std::filesystem::path{FESA_TEST_BINARY_DIR} / "src" / "fesa" / "Debug" /
|
||||
"fesa_solver.lib";
|
||||
std::error_code error;
|
||||
const auto solver_time =
|
||||
std::filesystem::last_write_time(solver_library, error);
|
||||
if (error ||
|
||||
!std::filesystem::is_directory(reference_case.execution_directory)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(
|
||||
reference_case.execution_directory, error)) {
|
||||
if (error) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (entry.path().filename() == "shellNL-meshfine-pressure_results.h5" &&
|
||||
std::filesystem::is_regular_file(entry.path()) &&
|
||||
std::filesystem::last_write_time(entry.path(), error) >= solver_time &&
|
||||
!error) {
|
||||
candidates.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
std::sort(candidates.begin(), candidates.end());
|
||||
return candidates.empty()
|
||||
? std::nullopt
|
||||
: std::optional<std::filesystem::path>{candidates.back()};
|
||||
}
|
||||
|
||||
const fesa::test::NonlinearReferenceComparisonReport& RunPressureComparison() {
|
||||
static const auto report = [] {
|
||||
const auto reference_case = MakePressureCase();
|
||||
const auto cached = FindCachedResults(reference_case);
|
||||
if (cached.has_value()) {
|
||||
const auto comparison =
|
||||
fesa::test::CompareHdf5ToReferenceCsv(reference_case, *cached);
|
||||
if (comparison.HasValue()) {
|
||||
return comparison.Value();
|
||||
}
|
||||
}
|
||||
const auto copied =
|
||||
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
if (!copied.HasValue()) {
|
||||
throw std::runtime_error{"Unable to copy pressure reference input."};
|
||||
}
|
||||
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
|
||||
reference_case, copied.Value());
|
||||
if (!generated.HasValue()) {
|
||||
throw std::runtime_error{"Unable to run pressure reference input."};
|
||||
}
|
||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
||||
reference_case, generated.Value().generated_results_hdf5);
|
||||
if (!comparison.HasValue()) {
|
||||
throw std::runtime_error{"Unable to compare pressure reference output."};
|
||||
}
|
||||
return comparison.Value();
|
||||
}();
|
||||
return report;
|
||||
}
|
||||
|
||||
const fesa::test::NonlinearReferenceFamilyReport& FindFamily(
|
||||
const fesa::test::NonlinearReferenceComparisonReport& report,
|
||||
const std::string& identity) {
|
||||
const auto found = std::find_if(
|
||||
report.families.begin(), report.families.end(),
|
||||
[&identity](const fesa::test::NonlinearReferenceFamilyReport& family) {
|
||||
return family.family_identity == identity;
|
||||
});
|
||||
if (found == report.families.end()) {
|
||||
throw std::runtime_error{"Required comparison family is absent."};
|
||||
}
|
||||
return *found;
|
||||
}
|
||||
|
||||
fesa::test::ReferenceToleranceFamilyDecision MakeTolerance(const bool passed) {
|
||||
fesa::test::ReferenceToleranceFamilyDecision result{};
|
||||
result.passed = passed;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ShellSurfacePressureReference,
|
||||
AuditsExactRequiredArtifactsAndIgnoresOptionalReviewArtifacts) {
|
||||
const auto artifacts = fesa::test::ArtifactCheck(MakePressureCase());
|
||||
|
||||
ASSERT_TRUE(artifacts.HasValue());
|
||||
EXPECT_EQ(MakePressureCase().reaction_artifact_disposition,
|
||||
fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly);
|
||||
EXPECT_EQ(MakePressureCase().stress_artifact_disposition,
|
||||
fesa::test::NonlinearReferenceArtifactDisposition::kExcluded);
|
||||
ASSERT_EQ(artifacts.Value().size(), 2U);
|
||||
EXPECT_EQ(artifacts.Value()[0U].byte_size, 56665U);
|
||||
EXPECT_EQ(artifacts.Value()[0U].sha256,
|
||||
"53718CCC9B41C23FCC1B509A51D66F1347E87ACA43A74F1D6CEC4D2085F4B90E");
|
||||
EXPECT_EQ(artifacts.Value()[1U].byte_size, 111182U);
|
||||
EXPECT_EQ(artifacts.Value()[1U].sha256,
|
||||
"2FE53F98EAC9F04779100721C3EDF8049EB1DBFE733EBCBD819AEEED03B81198");
|
||||
}
|
||||
|
||||
TEST(ShellSurfacePressureReference,
|
||||
SelectsUniqueSchemaOneFactorOneAcceptedFrame) {
|
||||
const auto& report = RunPressureComparison();
|
||||
|
||||
EXPECT_TRUE(report.blocking_passed);
|
||||
EXPECT_EQ(report.case_id, "shellnl-meshfine-pressure-s4-follower");
|
||||
}
|
||||
|
||||
TEST(ShellSurfacePressureReference,
|
||||
Projects729WideRowsTo2187LogicalRowsPerFamily) {
|
||||
const auto& report = RunPressureComparison();
|
||||
|
||||
EXPECT_EQ(FindFamily(report, "pressure_nodal_translation_blocking").row_count,
|
||||
2187U);
|
||||
EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").row_count,
|
||||
2187U);
|
||||
}
|
||||
|
||||
TEST(ShellSurfacePressureReference,
|
||||
MatchesByInstanceNodeLabelAndComponentNotRowOrder) {
|
||||
const auto& report = RunPressureComparison();
|
||||
|
||||
const auto& translation =
|
||||
FindFamily(report, "pressure_nodal_translation_blocking");
|
||||
EXPECT_EQ(translation.components,
|
||||
(std::vector<std::string>{"U1", "U2", "U3"}));
|
||||
EXPECT_EQ(translation.tolerance.rows.size(), 2187U);
|
||||
}
|
||||
|
||||
TEST(
|
||||
ShellSurfacePressureReference,
|
||||
RejectsSchemaIdentityCardinalityMissingExtraDuplicateAndNonfiniteBeforeTolerance) {
|
||||
const auto& report = RunPressureComparison();
|
||||
const std::filesystem::path invalid_hdf5 =
|
||||
report.generated_results_hdf5.parent_path() / "invalid-pressure.h5";
|
||||
{
|
||||
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
|
||||
stream << "not an HDF5 file";
|
||||
}
|
||||
EXPECT_FALSE(
|
||||
fesa::test::CompareHdf5ToReferenceCsv(MakePressureCase(), invalid_hdf5)
|
||||
.HasValue());
|
||||
|
||||
fesa::test::NonlinearReferenceComparisonReport precheck_report{};
|
||||
precheck_report.blocking_passed = true;
|
||||
precheck_report.precheck_findings = {
|
||||
{"pressure_nodal_translation_blocking",
|
||||
fesa::test::NonlinearReferenceDisposition::kBlocking,
|
||||
"missing-extra-duplicate-or-nonfinite",
|
||||
{}}};
|
||||
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(precheck_report),
|
||||
fesa::test::NonlinearReferenceClassification::kFailed);
|
||||
}
|
||||
|
||||
TEST(ShellSurfacePressureReference,
|
||||
ClassifiesTranslationBlockingAndRotationWarningOnly) {
|
||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||
@@ -207,55 +24,9 @@ TEST(ShellSurfacePressureReference,
|
||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||
2187U,
|
||||
MakeTolerance(false)}};
|
||||
|
||||
EXPECT_EQ(
|
||||
fesa::test::ClassifyReferenceComparison(report),
|
||||
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
|
||||
}
|
||||
|
||||
TEST(ShellSurfacePressureReference,
|
||||
ExcludesReactionAndStressFamiliesFromVerdict) {
|
||||
const auto& report = RunPressureComparison();
|
||||
|
||||
EXPECT_EQ(report.families.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
FindFamily(report, "pressure_nodal_translation_blocking").disposition,
|
||||
fesa::test::NonlinearReferenceDisposition::kBlocking);
|
||||
EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").disposition,
|
||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly);
|
||||
}
|
||||
|
||||
TEST(ShellSurfacePressureReference,
|
||||
UsesUnchangedCommonScaleNearZeroRelativeAndRmsPolicy) {
|
||||
const auto& translation = FindFamily(RunPressureComparison(),
|
||||
"pressure_nodal_translation_blocking");
|
||||
|
||||
EXPECT_EQ(translation.tolerance.near_zero_band,
|
||||
fesa::test::ReferenceTolerancePolicy::kNearZeroRatio *
|
||||
translation.tolerance.reference_scale);
|
||||
EXPECT_LE(translation.tolerance.relative_rms,
|
||||
fesa::test::ReferenceTolerancePolicy::kRelativeRmsTolerance);
|
||||
EXPECT_TRUE(translation.tolerance.rms_passed);
|
||||
}
|
||||
|
||||
TEST(ShellSurfacePressureReference, WritesDeterministicComparisonReport) {
|
||||
const auto& first = RunPressureComparison();
|
||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
||||
MakePressureCase(), first.generated_results_hdf5);
|
||||
|
||||
ASSERT_TRUE(comparison.HasValue());
|
||||
ASSERT_EQ(first.families.size(), comparison.Value().families.size());
|
||||
for (std::size_t index = 0U; index < first.families.size(); ++index) {
|
||||
const auto& expected = first.families[index];
|
||||
const auto& actual = comparison.Value().families[index];
|
||||
EXPECT_EQ(actual.family_identity, expected.family_identity);
|
||||
EXPECT_EQ(actual.components, expected.components);
|
||||
EXPECT_EQ(actual.disposition, expected.disposition);
|
||||
EXPECT_EQ(actual.row_count, expected.row_count);
|
||||
EXPECT_DOUBLE_EQ(actual.tolerance.reference_scale,
|
||||
expected.tolerance.reference_scale);
|
||||
EXPECT_DOUBLE_EQ(actual.tolerance.relative_rms,
|
||||
expected.tolerance.relative_rms);
|
||||
EXPECT_EQ(actual.tolerance.worst_row, expected.tolerance.worst_row);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#include "analysis/eigen_reduction.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
|
||||
|
||||
namespace {
|
||||
|
||||
fesa::SparseMatrix Sparse(const fesa::Matrix& dense) {
|
||||
fesa::SparsePattern pattern;
|
||||
std::vector<fesa::CooContribution> entries;
|
||||
pattern.row_offsets.push_back(0U);
|
||||
for (std::size_t i = 0; i < dense.Rows(); ++i) {
|
||||
for (std::size_t j = 0; j < dense.Columns(); ++j) {
|
||||
pattern.column_indices.push_back(j);
|
||||
entries.push_back({i, j, dense(i, j), 0U, i * dense.Columns() + j});
|
||||
}
|
||||
pattern.row_offsets.push_back(pattern.column_indices.size());
|
||||
}
|
||||
auto result = fesa::SparseMatrix::FromCoo(dense.Rows(), dense.Columns(),
|
||||
std::move(entries), pattern);
|
||||
EXPECT_TRUE(result.HasValue());
|
||||
return std::move(result.Value());
|
||||
}
|
||||
|
||||
fesa::Matrix NodalMass(const std::array<double, 3>& d) {
|
||||
fesa::Matrix mass(6U, 6U);
|
||||
for (std::size_t i = 0; i < 3U; ++i) {
|
||||
mass(i, i) = 2.0;
|
||||
for (std::size_t j = 0; j < 3U; ++j) {
|
||||
mass(i + 3U, j + 3U) = (static_cast<double>(i == j) - d[i] * d[j]) / 6.0;
|
||||
}
|
||||
}
|
||||
return mass;
|
||||
}
|
||||
|
||||
fesa::ShellNodeInitialFrame TiltedFrame() {
|
||||
return {0U, {0.6, 0.0, 0.8}, {0.0, 1.0, 0.0}, {-0.8, 0.0, 0.6}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(EigenReduction, KeepsPositivePartialGlobalRotationAndOriginalPencil) {
|
||||
const auto frame = TiltedFrame();
|
||||
fesa::Matrix stiffness(6U, 6U);
|
||||
stiffness(5U, 5U) = 10369.0 / 7500.0;
|
||||
std::vector<std::uint8_t> mask(6U, 1U);
|
||||
mask[5U] = 0U;
|
||||
const auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {NodalMass(frame.director)}, {frame}, mask,
|
||||
{19.0 / 4800.0}, 4.0);
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
EXPECT_EQ(result.Value().null_count, 0U);
|
||||
EXPECT_EQ(result.Value().whitened_operator.Rows(), 1U);
|
||||
EXPECT_NEAR(result.Value().whitened_operator.Values()[0], 10369.0 / 450.0,
|
||||
1e-12);
|
||||
fesa::Matrix x(1U, 1U, 1.0);
|
||||
const auto y = fesa::eigen_internal::RestoreScaledMode(result.Value(), x, 0U);
|
||||
for (std::size_t i = 0; i < 5U; ++i) EXPECT_DOUBLE_EQ(y[i], 0.0);
|
||||
EXPECT_NEAR(y[5U] * y[5U] * (3.0 / 50.0), 1.0, 1e-12);
|
||||
}
|
||||
|
||||
TEST(EigenReduction, RemovesOnlyActualFreeDirectorNull) {
|
||||
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
|
||||
auto mass = NodalMass(frame.director);
|
||||
fesa::Matrix stiffness(6U, 6U);
|
||||
for (std::size_t i = 0; i < 5U; ++i) stiffness(i, i) = mass(i, i) * 7.0;
|
||||
stiffness(5U, 5U) = 0.25;
|
||||
const auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {mass}, {frame}, std::vector<std::uint8_t>(6U, 0U),
|
||||
{0.25}, 2.0);
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
EXPECT_EQ(result.Value().free_count, 6U);
|
||||
EXPECT_EQ(result.Value().null_count, 1U);
|
||||
EXPECT_EQ(result.Value().whitened_operator.Rows(), 5U);
|
||||
fesa::Vector ones(5U, 1.0);
|
||||
const auto action = result.Value().whitened_operator.Multiply(ones);
|
||||
for (std::size_t i = 0; i < 5U; ++i) EXPECT_NEAR(action[i], 7.0, 1e-12);
|
||||
EXPECT_LE(result.Value().eta_kz, 1e-12);
|
||||
stiffness(0U, 5U) = stiffness(5U, 0U) = 0.1;
|
||||
EXPECT_FALSE(fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {mass}, {frame},
|
||||
std::vector<std::uint8_t>(6U, 0U), {0.25}, 2.0)
|
||||
.HasValue());
|
||||
}
|
||||
|
||||
TEST(EigenReduction, RejectsPositiveButUnresolvableMassWithoutDeletingDofs) {
|
||||
const double p = 1e-8;
|
||||
const std::array<double, 3> d{2 * p / (1 + p * p), 0,
|
||||
(1 - p * p) / (1 + p * p)};
|
||||
const fesa::ShellNodeInitialFrame frame{0U, d, {0, 1, 0}, {-d[2], 0, d[0]}};
|
||||
fesa::Matrix stiffness(6U, 6U);
|
||||
for (std::size_t i = 0; i < 6U; ++i) stiffness(i, i) = 1.0;
|
||||
std::vector<std::uint8_t> mask(6U, 0U);
|
||||
mask[3U] = 1U;
|
||||
const auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {NodalMass(d)}, {frame}, mask, {0.25}, 1.0);
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
EXPECT_EQ(result.GetStatus().Diagnostics()[0].code,
|
||||
"eigen-mass-conditioning");
|
||||
}
|
||||
|
||||
TEST(EigenReduction, WhitensOffDiagonalMassAndRestoresOriginalPencil) {
|
||||
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
|
||||
auto mass = NodalMass(frame.director);
|
||||
mass(0, 1) = mass(1, 0) = 1.0;
|
||||
fesa::Matrix stiffness(6U, 6U);
|
||||
stiffness(0, 0) = stiffness(1, 1) = 6.0;
|
||||
stiffness(0, 1) = stiffness(1, 0) = 4.0;
|
||||
std::vector<std::uint8_t> mask{0U, 0U, 1U, 1U, 1U, 1U};
|
||||
const auto reduction = fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {mass}, {frame}, mask, {0.25}, 3.0);
|
||||
ASSERT_TRUE(reduction.HasValue());
|
||||
const auto eigen = fesa::MklFeastEigenSolver{}.Solve(
|
||||
reduction.Value().whitened_operator, 2U, 1.0);
|
||||
ASSERT_TRUE(eigen.HasValue());
|
||||
ASSERT_EQ(eigen.Value().eigenvalues.size(), 2U);
|
||||
EXPECT_NEAR(eigen.Value().eigenvalues[0], 2.0, 1e-12);
|
||||
EXPECT_NEAR(eigen.Value().eigenvalues[1], 10.0 / 3.0, 1e-12);
|
||||
for (std::size_t mode = 0; mode < 2U; ++mode) {
|
||||
auto y = fesa::eigen_internal::RestoreScaledMode(
|
||||
reduction.Value(), eigen.Value().eigenvectors, mode);
|
||||
fesa::Vector q = y;
|
||||
for (std::size_t i = 0; i < 3U; ++i) q[i] *= 3.0;
|
||||
const auto mq = mass.Multiply(q);
|
||||
EXPECT_NEAR(q.Dot(mq), 1.0, 1e-12);
|
||||
auto residual = stiffness.Multiply(q);
|
||||
residual.Axpy(-eigen.Value().eigenvalues[mode], mq);
|
||||
EXPECT_LE(residual.Norm(), 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(EigenReduction, PreservesSmallResolvablePositivePartialRotationMass) {
|
||||
const double p = 1.0 / 2000.0;
|
||||
const std::array<double, 3> d{2 * p / (1 + p * p), 0,
|
||||
(1 - p * p) / (1 + p * p)};
|
||||
const fesa::ShellNodeInitialFrame frame{0U, d, {0, 1, 0}, {-d[2], 0, d[0]}};
|
||||
fesa::Matrix stiffness(6U, 6U);
|
||||
for (std::size_t i = 0; i < 6U; ++i) stiffness(i, i) = 1.0;
|
||||
std::vector<std::uint8_t> mask(6U, 0U);
|
||||
mask[3U] = 1U;
|
||||
const auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {NodalMass(d)}, {frame}, mask, {0.25}, 1.0);
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
EXPECT_EQ(result.Value().null_count, 0U);
|
||||
EXPECT_EQ(result.Value().whitened_operator.Rows(), 5U);
|
||||
EXPECT_GE(result.Value().nodes[0].record.rcond_inf, 0x1p-32);
|
||||
}
|
||||
|
||||
TEST(EigenReduction, HandlesTwoFreeRotationsAndRejectsEmptyFiniteSpace) {
|
||||
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
|
||||
auto mass = NodalMass(frame.director);
|
||||
fesa::Matrix stiffness(6U, 6U);
|
||||
stiffness(3U, 3U) = 1.0;
|
||||
stiffness(5U, 5U) = 0.25;
|
||||
std::vector<std::uint8_t> mask{1U, 1U, 1U, 0U, 1U, 0U};
|
||||
auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {mass}, {frame}, mask, {0.25}, 1.0);
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
EXPECT_EQ(result.Value().null_count, 1U);
|
||||
EXPECT_EQ(result.Value().whitened_operator.Rows(), 1U);
|
||||
mass(5U, 5U) = 0.1;
|
||||
EXPECT_FALSE(fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {mass}, {frame}, mask, {0.25}, 1.0)
|
||||
.HasValue());
|
||||
mass(5U, 5U) = 0.0;
|
||||
for (const bool drill_free : {false, true}) {
|
||||
mask.assign(6U, 1U);
|
||||
mask[5U] = drill_free ? 0U : 1U;
|
||||
const auto empty = fesa::eigen_internal::BuildEigenReduction(
|
||||
Sparse(stiffness), {mass}, {frame}, mask, {0.25}, 1.0);
|
||||
ASSERT_FALSE(empty.HasValue());
|
||||
EXPECT_EQ(empty.GetStatus().Category(), fesa::FailureCategory::kModel);
|
||||
EXPECT_EQ(empty.GetStatus().Diagnostics()[0].code, "empty-eigenproblem");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "fesa/model/model_types.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using Coordinates = std::array<double, 3>;
|
||||
|
||||
fesa::Node Node(std::int64_t label, Coordinates coordinates) {
|
||||
return {{"I", label, std::to_string(label)}, coordinates, {"mass.inp", 1U}};
|
||||
}
|
||||
|
||||
std::array<const fesa::Node*, 4> Pointers(
|
||||
const std::array<fesa::Node, 4>& nodes) {
|
||||
return {&nodes[0], &nodes[1], &nodes[2], &nodes[3]};
|
||||
}
|
||||
|
||||
fesa::Mitc4Shell WarpedShell() {
|
||||
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}}}};
|
||||
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 result =
|
||||
fesa::Mitc4Shell::Create(Pointers(nodes), directors, section, material);
|
||||
EXPECT_TRUE(result.HasValue());
|
||||
return result.Value();
|
||||
}
|
||||
|
||||
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.
|
||||
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];
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// E2-C03: scalar row-sum lumping retains uniform translation energy and is
|
||||
// never below the consistent kinetic energy.
|
||||
TEST(Mitc4ShellMassContract, SatisfiesUniformAndJensenEnergyContracts) {
|
||||
const auto mass = WarpedShell().Mass(3.0);
|
||||
ASSERT_TRUE(mass.HasValue());
|
||||
fesa::Vector translation{24U};
|
||||
fesa::Vector field{24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
translation[6U * node] = 1.0;
|
||||
field[6U * node] = static_cast<double>(node + 1U);
|
||||
field[6U * node + 1U] = -0.5 * static_cast<double>(node);
|
||||
field[6U * node + 3U] = 0.25;
|
||||
}
|
||||
EXPECT_NEAR(Quadratic(mass.Value().consistent_global24, translation),
|
||||
Quadratic(mass.Value().lumped_global24, translation), 1e-12);
|
||||
EXPECT_GE(Quadratic(mass.Value().lumped_global24, field) + 1e-12,
|
||||
Quadratic(mass.Value().consistent_global24, field));
|
||||
}
|
||||
|
||||
// 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}},
|
||||
{{0.0, 0.0, 1.0}},
|
||||
{{0.0, 0.0, 1.0}},
|
||||
{{0.0, 0.0, 1.0}}}};
|
||||
const fesa::ShellSection section{"S", 1.0, 0U, {"mass.inp", 2U}};
|
||||
const fesa::LinearElasticMaterial material{"M", 12.0, 0.0, {"mass.inp", 3U}};
|
||||
for (const int mesh : {1, 2, 4}) {
|
||||
double consistent_work = 0.0, lumped_work = 0.0;
|
||||
const double edge = 2.0 / mesh;
|
||||
for (int row = 0; row < mesh; ++row) {
|
||||
for (int column = 0; column < mesh; ++column) {
|
||||
const double x = -1.0 + edge * column, y = -1.0 + edge * row;
|
||||
const std::array<fesa::Node, 4> nodes{
|
||||
Node(1, {x, y, 0.0}), Node(2, {x + edge, y, 0.0}),
|
||||
Node(3, {x + edge, y + edge, 0.0}), Node(4, {x, y + edge, 0.0})};
|
||||
const auto shell = fesa::Mitc4Shell::Create(Pointers(nodes), directors,
|
||||
section, material);
|
||||
ASSERT_TRUE(shell.HasValue());
|
||||
const auto mass = shell.Value().Mass(1.0);
|
||||
ASSERT_TRUE(mass.HasValue());
|
||||
fesa::Vector velocity{24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
velocity[6U * node] = nodes[node].coordinates[0U];
|
||||
consistent_work +=
|
||||
Quadratic(mass.Value().consistent_global24, velocity);
|
||||
lumped_work += Quadratic(mass.Value().lumped_global24, velocity);
|
||||
}
|
||||
}
|
||||
EXPECT_NEAR(consistent_work, 4.0 / 3.0, 1e-12) << mesh;
|
||||
EXPECT_NEAR(lumped_work - consistent_work, 8.0 / (3.0 * mesh * mesh), 1e-12)
|
||||
<< mesh;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -53,6 +53,12 @@ Vector3 Cross(const Vector3& left, const Vector3& right) {
|
||||
left[0] * right[1] - left[1] * right[0]};
|
||||
}
|
||||
|
||||
Vector3 RotateAboutGlobalZ(const Vector3& value) {
|
||||
constexpr double kInverseSqrtTwo = 0.70710678118654752440;
|
||||
return {kInverseSqrtTwo * (value[0U] - value[1U]),
|
||||
kInverseSqrtTwo * (value[0U] + value[1U]), value[2U]};
|
||||
}
|
||||
|
||||
double Norm(const Vector3& value) { return std::sqrt(Dot(value, value)); }
|
||||
|
||||
void ExpectVectorNear(const Vector3& actual, const Vector3& expected,
|
||||
@@ -636,7 +642,6 @@ TEST(Mitc4ShellKernel,
|
||||
EXPECT_DOUBLE_EQ(stiffness.physical_local20(0U, 0U), 0x1.d555555555554p+6);
|
||||
EXPECT_DOUBLE_EQ(stiffness.physical_local20(0U, 4U), 0.0);
|
||||
EXPECT_DOUBLE_EQ(stiffness.physical_global24(2U, 2U), 0x1.aaaaaaaaaaaadp+5);
|
||||
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 0x1.0d6cffc5beeb5p-4);
|
||||
EXPECT_DOUBLE_EQ(recovery.strain_energy, 0x1.20a3d70a3d70bp+6);
|
||||
constexpr std::array<double, 8> kExpectedStrain{
|
||||
0x1.999999999999ap-4, -0x1.999999999999bp-5, 0x1.9999999999998p-3,
|
||||
@@ -791,8 +796,7 @@ TEST(Mitc4ShellPatch, ReproducesIndependentMembraneBendingShearAndTwistFields) {
|
||||
}
|
||||
|
||||
// MITC4-KERNEL-005
|
||||
TEST(Mitc4ShellDrilling,
|
||||
UsesOnlyEightPositivePhysicalRotationDiagonalsAndFixedFactor) {
|
||||
TEST(Mitc4ShellDrilling, UsesAllPhysicalRotationDiagonalsAndFixedFactor) {
|
||||
const std::array<fesa::Node, 4> nodes{
|
||||
Node(1, {-50.0, -50.0, 0.0}), Node(2, {50.0, -50.0, 0.0}),
|
||||
Node(3, {50.0, 50.0, 0.0}), Node(4, {-50.0, 50.0, 0.0})};
|
||||
@@ -803,27 +807,222 @@ TEST(Mitc4ShellDrilling,
|
||||
ASSERT_TRUE(stiffness_candidate.HasValue());
|
||||
const auto& stiffness = stiffness_candidate.Value();
|
||||
|
||||
double expected_reference = (std::numeric_limits<double>::max)();
|
||||
double all_diagonal_minimum = (std::numeric_limits<double>::max)();
|
||||
double expected_sum = 0.0;
|
||||
for (std::size_t node_index = 0U; node_index < 4U; ++node_index) {
|
||||
for (std::size_t rotation = 3U; rotation < 5U; ++rotation) {
|
||||
const std::size_t index = 5U * node_index + rotation;
|
||||
const double diagonal = stiffness.physical_local20(index, index);
|
||||
ASSERT_TRUE(std::isfinite(diagonal));
|
||||
ASSERT_GT(diagonal, 0.0);
|
||||
expected_reference = (std::min)(expected_reference, diagonal);
|
||||
}
|
||||
for (std::size_t component = 0U; component < 5U; ++component) {
|
||||
const std::size_t index = 5U * node_index + component;
|
||||
const double diagonal = stiffness.physical_local20(index, index);
|
||||
if (std::isfinite(diagonal) && diagonal > 0.0) {
|
||||
all_diagonal_minimum = (std::min)(all_diagonal_minimum, diagonal);
|
||||
}
|
||||
ASSERT_GE(diagonal, 0.0);
|
||||
expected_sum += diagonal;
|
||||
}
|
||||
}
|
||||
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 1.0e-3 * expected_reference);
|
||||
EXPECT_LT(all_diagonal_minimum, expected_reference);
|
||||
EXPECT_NE(stiffness.drilling_stiffness, 1.0e-3 * all_diagonal_minimum);
|
||||
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 1.0e-3 * expected_sum / 8.0);
|
||||
}
|
||||
|
||||
TEST(Mitc4ShellMass, IntegratesFlatConsistentAndScalarRowSumLumpedMass) {
|
||||
const auto nodes = PlanarNodes();
|
||||
const auto shell_candidate = fesa::Mitc4Shell::Create(
|
||||
NodePointers(nodes), Directors(), Section(), Material());
|
||||
ASSERT_TRUE(shell_candidate.HasValue());
|
||||
|
||||
const auto mass_candidate = shell_candidate.Value().Mass(3.0);
|
||||
ASSERT_TRUE(mass_candidate.HasValue());
|
||||
const auto& mass = mass_candidate.Value();
|
||||
const std::array<std::array<double, 4>, 4> pattern{{{{4.0, 2.0, 1.0, 2.0}},
|
||||
{{2.0, 4.0, 2.0, 1.0}},
|
||||
{{1.0, 2.0, 4.0, 2.0}},
|
||||
{{2.0, 1.0, 2.0, 4.0}}}};
|
||||
for (std::size_t row = 0U; row < 4U; ++row) {
|
||||
EXPECT_NEAR(mass.nodal_moments[row][0U], 6.0, 1.0e-12);
|
||||
EXPECT_NEAR(mass.nodal_moments[row][1U], 0.0, 1.0e-12);
|
||||
EXPECT_NEAR(mass.nodal_moments[row][2U], 2.0, 1.0e-12);
|
||||
for (std::size_t column = 0U; column < 4U; ++column) {
|
||||
EXPECT_NEAR(mass.consistent_moments[0U](row, column),
|
||||
pattern[row][column] * (2.0 / 3.0), 1.0e-12);
|
||||
EXPECT_NEAR(mass.consistent_moments[1U](row, column), 0.0, 1.0e-12);
|
||||
EXPECT_NEAR(mass.consistent_moments[2U](row, column),
|
||||
pattern[row][column] * (2.0 / 9.0), 1.0e-12);
|
||||
}
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
EXPECT_NEAR(
|
||||
mass.lumped_global24(6U * row + component, 6U * row + component), 6.0,
|
||||
1.0e-12);
|
||||
EXPECT_NEAR(mass.lumped_global24(6U * row + 3U + component,
|
||||
6U * row + 3U + component),
|
||||
component == 2U ? 0.0 : 2.0, 1.0e-12);
|
||||
}
|
||||
}
|
||||
ExpectSymmetric(mass.consistent_global24);
|
||||
ExpectSymmetric(mass.lumped_global24);
|
||||
}
|
||||
|
||||
TEST(Mitc4ShellMass, RejectsNonpositiveAndNonfiniteDensity) {
|
||||
const auto nodes = PlanarNodes();
|
||||
const auto shell_candidate = fesa::Mitc4Shell::Create(
|
||||
NodePointers(nodes), Directors(), Section(), Material());
|
||||
ASSERT_TRUE(shell_candidate.HasValue());
|
||||
for (const double density :
|
||||
{0.0, -1.0, std::numeric_limits<double>::infinity()}) {
|
||||
const auto mass = shell_candidate.Value().Mass(density);
|
||||
ASSERT_FALSE(mass.HasValue());
|
||||
ASSERT_EQ(mass.GetStatus().Diagnostics().size(), 1U);
|
||||
EXPECT_EQ(mass.GetStatus().Diagnostics()[0U].code, "invalid-shell-mass");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Mitc4ShellMass, PreservesFiniteNodalVarianceAcrossDensityScale) {
|
||||
const auto nodes = PlanarNodes();
|
||||
const auto shell_candidate = fesa::Mitc4Shell::Create(
|
||||
NodePointers(nodes), Directors(), Section(), Material());
|
||||
ASSERT_TRUE(shell_candidate.HasValue());
|
||||
for (const double density : {1.0e-180, 1.0e180}) {
|
||||
const auto mass_candidate = shell_candidate.Value().Mass(density);
|
||||
ASSERT_TRUE(mass_candidate.HasValue());
|
||||
for (const auto& moments : mass_candidate.Value().nodal_moments) {
|
||||
EXPECT_TRUE(std::isfinite(moments[0U]));
|
||||
EXPECT_TRUE(std::isfinite(moments[1U]));
|
||||
EXPECT_TRUE(std::isfinite(moments[2U]));
|
||||
EXPECT_GT(moments[2U] - moments[1U] * (moments[1U] / moments[0U]), 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectGlobalZCovariance(const fesa::Matrix& original,
|
||||
const fesa::Matrix& rotated) {
|
||||
ASSERT_EQ(original.Rows(), 24U);
|
||||
ASSERT_EQ(original.Columns(), 24U);
|
||||
ASSERT_EQ(rotated.Rows(), 24U);
|
||||
ASSERT_EQ(rotated.Columns(), 24U);
|
||||
constexpr double kInverseSqrtTwo = 0.70710678118654752440;
|
||||
const std::array<std::array<double, 3>, 3> transform{
|
||||
{{{kInverseSqrtTwo, -kInverseSqrtTwo, 0.0}},
|
||||
{{kInverseSqrtTwo, kInverseSqrtTwo, 0.0}},
|
||||
{{0.0, 0.0, 1.0}}}};
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||
double expected = 0.0;
|
||||
const std::size_t row_offset =
|
||||
6U * (row / 6U) + (row % 6U < 3U ? 0U : 3U);
|
||||
const std::size_t column_offset =
|
||||
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 first = 0U; first < 3U; ++first) {
|
||||
for (std::size_t second = 0U; second < 3U; ++second) {
|
||||
expected += transform[row_component][first] *
|
||||
original(row_offset + first, column_offset + second) *
|
||||
transform[column_component][second];
|
||||
}
|
||||
}
|
||||
EXPECT_NEAR(rotated(row, column), expected, 1.0e-12)
|
||||
<< "at (" << row << ", " << column << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Mitc4ShellMass, PreservesIndependentWarpedMomentOracle) {
|
||||
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<Vector3, 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 auto shell_candidate = fesa::Mitc4Shell::Create(
|
||||
NodePointers(nodes), directors, Section(0.5), Material());
|
||||
ASSERT_TRUE(shell_candidate.HasValue());
|
||||
const auto mass_candidate = shell_candidate.Value().Mass(3.0);
|
||||
ASSERT_TRUE(mass_candidate.HasValue());
|
||||
const auto& mass = mass_candidate.Value();
|
||||
|
||||
EXPECT_NEAR(mass.consistent_moments[0U](0U, 0U), 17609.0 / 28800.0, 1.0e-12);
|
||||
EXPECT_NEAR(mass.consistent_moments[1U](0U, 0U), 11003.0 / 1440000.0,
|
||||
1.0e-12);
|
||||
EXPECT_NEAR(mass.consistent_moments[2U](0U, 0U), 88177.0 / 6912000.0,
|
||||
1.0e-12);
|
||||
double total_mass = 0.0;
|
||||
double total_first_moment = 0.0;
|
||||
double total_second_moment = 0.0;
|
||||
for (const auto& moments : mass.nodal_moments) {
|
||||
total_mass += moments[0U];
|
||||
total_first_moment += moments[1U];
|
||||
total_second_moment += moments[2U];
|
||||
}
|
||||
EXPECT_NEAR(total_mass, 7743.0 / 1600.0, 1.0e-12);
|
||||
EXPECT_NEAR(total_first_moment, 739.0 / 12000.0, 1.0e-12);
|
||||
EXPECT_NEAR(total_second_moment, 517.0 / 5120.0, 1.0e-12);
|
||||
}
|
||||
|
||||
TEST(Mitc4ShellMass, MeetsPartialBoundaryCompleteMatrixTargets) {
|
||||
const Vector3 tangent_a{0.0, 1.0, 0.0};
|
||||
const Vector3 tangent_b{-0.8, 0.0, 0.6};
|
||||
const std::array<fesa::Node, 4> nodes{
|
||||
Node(1, {-2.0 * tangent_a[0U] - tangent_b[0U],
|
||||
-2.0 * tangent_a[1U] - tangent_b[1U],
|
||||
-2.0 * tangent_a[2U] - tangent_b[2U]}),
|
||||
Node(2, {2.0 * tangent_a[0U] - tangent_b[0U],
|
||||
2.0 * tangent_a[1U] - tangent_b[1U],
|
||||
2.0 * tangent_a[2U] - tangent_b[2U]}),
|
||||
Node(3, {2.0 * tangent_a[0U] + tangent_b[0U],
|
||||
2.0 * tangent_a[1U] + tangent_b[1U],
|
||||
2.0 * tangent_a[2U] + tangent_b[2U]}),
|
||||
Node(4, {-2.0 * tangent_a[0U] + tangent_b[0U],
|
||||
-2.0 * tangent_a[1U] + tangent_b[1U],
|
||||
-2.0 * tangent_a[2U] + tangent_b[2U]})};
|
||||
const auto shell_candidate =
|
||||
fesa::Mitc4Shell::Create(NodePointers(nodes), Directors({0.6, 0.0, 0.8}),
|
||||
Section(1.0), Material(12.0, 0.0));
|
||||
ASSERT_TRUE(shell_candidate.HasValue());
|
||||
const auto stiffness_candidate = shell_candidate.Value().Stiffness();
|
||||
const auto mass_candidate = shell_candidate.Value().Mass(1.0);
|
||||
ASSERT_TRUE(stiffness_candidate.HasValue());
|
||||
ASSERT_TRUE(mass_candidate.HasValue());
|
||||
const double complete_stiffness =
|
||||
stiffness_candidate.Value().stabilized_global24(5U, 5U);
|
||||
const double lumped_mass = mass_candidate.Value().lumped_global24(5U, 5U);
|
||||
EXPECT_NEAR(stiffness_candidate.Value().drilling_stiffness, 19.0 / 4800.0,
|
||||
1.0e-12);
|
||||
EXPECT_NEAR(complete_stiffness, 10369.0 / 7500.0, 1.0e-12);
|
||||
EXPECT_NEAR(lumped_mass, 3.0 / 50.0, 1.0e-12);
|
||||
EXPECT_NEAR(complete_stiffness / lumped_mass, 10369.0 / 450.0, 1.0e-12);
|
||||
}
|
||||
|
||||
TEST(Mitc4ShellMass, RecomputesCompleteMassAndStiffnessCovariantly) {
|
||||
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<Vector3, 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> rotated_nodes = nodes;
|
||||
std::array<Vector3, 4> rotated_directors{};
|
||||
for (std::size_t node = 0U; node < nodes.size(); ++node) {
|
||||
rotated_nodes[node].coordinates =
|
||||
RotateAboutGlobalZ(nodes[node].coordinates);
|
||||
rotated_directors[node] = RotateAboutGlobalZ(directors[node]);
|
||||
}
|
||||
const auto original = fesa::Mitc4Shell::Create(NodePointers(nodes), directors,
|
||||
Section(0.5), Material());
|
||||
const auto rotated = fesa::Mitc4Shell::Create(
|
||||
NodePointers(rotated_nodes), rotated_directors, Section(0.5), Material());
|
||||
ASSERT_TRUE(original.HasValue());
|
||||
ASSERT_TRUE(rotated.HasValue());
|
||||
const auto original_stiffness = original.Value().Stiffness();
|
||||
const auto rotated_stiffness = rotated.Value().Stiffness();
|
||||
const auto original_mass = original.Value().Mass(3.0);
|
||||
const auto rotated_mass = rotated.Value().Mass(3.0);
|
||||
ASSERT_TRUE(original_stiffness.HasValue());
|
||||
ASSERT_TRUE(rotated_stiffness.HasValue());
|
||||
ASSERT_TRUE(original_mass.HasValue());
|
||||
ASSERT_TRUE(rotated_mass.HasValue());
|
||||
ExpectGlobalZCovariance(original_stiffness.Value().stabilized_global24,
|
||||
rotated_stiffness.Value().stabilized_global24);
|
||||
ExpectGlobalZCovariance(original_mass.Value().consistent_global24,
|
||||
rotated_mass.Value().consistent_global24);
|
||||
ExpectGlobalZCovariance(original_mass.Value().lumped_global24,
|
||||
rotated_mass.Value().lumped_global24);
|
||||
}
|
||||
|
||||
// MITC4-KERNEL-006
|
||||
|
||||
@@ -157,6 +157,9 @@ TEST(DomainMappingComponents,
|
||||
EXPECT_EQ(domain.Steps()[0].Loads().size(), 1U);
|
||||
ASSERT_EQ(domain.Warnings().size(), 3U);
|
||||
EXPECT_EQ(domain.Warnings()[0].keyword, "PREPRINT");
|
||||
EXPECT_EQ(
|
||||
domain.Warnings()[0].message,
|
||||
"The allowlisted Abaqus keyword is ignored without semantic effect.");
|
||||
EXPECT_EQ(domain.Warnings()[1].keyword, "OUTPUT");
|
||||
EXPECT_EQ(domain.Warnings()[2].keyword, "NODE OUTPUT");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/io/abaqus/domain_mapper.h"
|
||||
#include "fesa/io/abaqus/input_reader.h"
|
||||
|
||||
namespace {
|
||||
|
||||
class TemporaryInputFile {
|
||||
public:
|
||||
TemporaryInputFile(const std::string& stem, const std::string& content)
|
||||
: path_{std::filesystem::temp_directory_path() /
|
||||
("fesa-eigen-domain-" + stem + ".inp")} {
|
||||
std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
|
||||
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
|
||||
if (!stream) {
|
||||
throw std::runtime_error{"Unable to create eigen domain fixture."};
|
||||
}
|
||||
}
|
||||
|
||||
~TemporaryInputFile() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path_, error);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Path() const noexcept { return path_; }
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
fesa::Result<fesa::Domain> MapText(const std::string& stem,
|
||||
const std::string& content) {
|
||||
const TemporaryInputFile input{stem, content};
|
||||
auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
|
||||
if (!parsed.HasValue()) {
|
||||
return fesa::Result<fesa::Domain>::Failure(parsed.GetStatus());
|
||||
}
|
||||
return fesa::AbaqusDomainMapper{}.Map(parsed.Value());
|
||||
}
|
||||
|
||||
std::string EigenDeck(const std::string& density = "2700.,",
|
||||
const std::string& frequency = "10, , , , ,") {
|
||||
return R"inp(*Part, name=ShellPart
|
||||
*Node
|
||||
1, 0., 0., 0.
|
||||
2, 1., 0., 0.
|
||||
3, 1., 1., 0.
|
||||
4, 0., 1., 0.
|
||||
*Element, type=S4R
|
||||
1, 1, 2, 3, 4
|
||||
*Elset, elset=ShellSet
|
||||
1
|
||||
*Shell Section, elset=ShellSet, material=Steel
|
||||
0.5, 5
|
||||
*End Part
|
||||
*Assembly, name=Assembly
|
||||
*Instance, name=Shell-1, part=ShellPart
|
||||
*End Instance
|
||||
*Nset, nset=Root, instance=Shell-1
|
||||
1
|
||||
*End Assembly
|
||||
*Material, name=Steel
|
||||
*Density
|
||||
)inp" + density +
|
||||
R"inp(
|
||||
*Elastic
|
||||
2.1e11, 0.3
|
||||
*Boundary
|
||||
Root, 1, 6
|
||||
*Step, name=Step-2, nlgeom=NO, perturbation
|
||||
*Frequency, eigensolver=Lanczos, sim, acoustic coupling=on, normalization=mass
|
||||
)inp" + frequency +
|
||||
R"inp(
|
||||
*Restart, write, frequency=0
|
||||
*Output, field
|
||||
*Node Output, variable=PRESELECT
|
||||
*Output, history
|
||||
*Energy Output, variable=ALL
|
||||
*End Step
|
||||
)inp";
|
||||
}
|
||||
|
||||
const fesa::Diagnostic* FindDiagnostic(const fesa::Status& status,
|
||||
const std::string& code) {
|
||||
for (const auto& diagnostic : status.Diagnostics()) {
|
||||
if (diagnostic.code == code) {
|
||||
return &diagnostic;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, PreservesFrequencyDensityAndSourceStepIdentity) {
|
||||
auto result = MapText("accepted", EigenDeck());
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
const fesa::Domain& domain = result.Value();
|
||||
|
||||
ASSERT_EQ(domain.Densities().size(), 1U);
|
||||
EXPECT_EQ(domain.Densities()[0].material_index, fesa::EntityIndex{0U});
|
||||
EXPECT_DOUBLE_EQ(domain.Densities()[0].rho, 2700.0);
|
||||
ASSERT_NE(domain.Frequency(), nullptr);
|
||||
EXPECT_EQ(domain.Frequency()->source_step_name_raw, "Step-2");
|
||||
EXPECT_EQ(domain.Frequency()->source_step_name_canonical, "STEP-2");
|
||||
EXPECT_EQ(domain.Frequency()->requested_mode_count, 10);
|
||||
EXPECT_TRUE(domain.Frequency()->perturbation_explicit);
|
||||
EXPECT_TRUE(domain.Frequency()->sim_explicit);
|
||||
ASSERT_EQ(domain.Steps().Size(), 1U);
|
||||
EXPECT_TRUE(domain.Steps()[0U].IsFrequency());
|
||||
EXPECT_EQ(domain.Steps()[0U].Frequency(), domain.Frequency());
|
||||
EXPECT_EQ(domain.Steps()[0U].Name(), "Step-2");
|
||||
ASSERT_EQ(domain.Steps()[0U].BoundaryConditions().size(), 6U);
|
||||
EXPECT_TRUE(domain.Steps()[0U].Loads().empty());
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, RejectsInvalidDensityAndFrequencySlots) {
|
||||
auto density = MapText("bad-density", EigenDeck("2700., 1."));
|
||||
ASSERT_FALSE(density.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(density.GetStatus(), "invalid-density-data"),
|
||||
nullptr);
|
||||
|
||||
auto slots = MapText("bad-slots", EigenDeck("2700.,", "10, 0"));
|
||||
ASSERT_FALSE(slots.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(slots.GetStatus(), "invalid-frequency-data"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, RejectsMissingActiveDensityAndNonzeroBoundary) {
|
||||
std::string missing_deck = EigenDeck();
|
||||
const std::string density_block = "*Density\n2700.,\n";
|
||||
const std::size_t density_location = missing_deck.find(density_block);
|
||||
ASSERT_NE(density_location, std::string::npos);
|
||||
missing_deck.erase(density_location, density_block.size());
|
||||
auto missing = MapText("missing-density", missing_deck);
|
||||
ASSERT_FALSE(missing.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(missing.GetStatus(), "missing-material-density"),
|
||||
nullptr);
|
||||
|
||||
std::string nonzero = EigenDeck();
|
||||
const std::string source = "Root, 1, 6";
|
||||
const std::size_t location = nonzero.rfind(source);
|
||||
ASSERT_NE(location, std::string::npos);
|
||||
nonzero.replace(location, source.size(), "Root, 1, 6, 1.");
|
||||
auto boundary = MapText("nonzero-boundary", nonzero);
|
||||
ASSERT_FALSE(boundary.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(boundary.GetStatus(), "unsupported-eigen-boundary"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, RejectsMixedProceduresInvalidOutputAndSurface) {
|
||||
std::string mixed = EigenDeck();
|
||||
mixed.insert(mixed.find("*Restart"), "*Static\n0.1, 1., 0.01, 1.\n");
|
||||
auto mixed_result = MapText("mixed-procedures", mixed);
|
||||
ASSERT_FALSE(mixed_result.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(mixed_result.GetStatus(),
|
||||
"unsupported-analysis-procedure"),
|
||||
nullptr);
|
||||
|
||||
auto wrong_output = MapText(
|
||||
"wrong-output",
|
||||
EigenDeck().replace(EigenDeck().find("*Output, field\n*Node Output"),
|
||||
std::string{"*Output, field\n*Node Output"}.size(),
|
||||
"*Output, history\n*Node Output"));
|
||||
ASSERT_FALSE(wrong_output.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(wrong_output.GetStatus(), "unsupported-keyword"),
|
||||
nullptr);
|
||||
|
||||
std::string surface = EigenDeck();
|
||||
surface.insert(surface.find("*Material"),
|
||||
"*Surface, name=Face, type=ELEMENT\n");
|
||||
auto surface_result = MapText("surface", surface);
|
||||
ASSERT_FALSE(surface_result.HasValue());
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, RejectsInvalidPreprintAndAcceptsStrictNoOp) {
|
||||
const std::string preprint =
|
||||
"*Preprint, echo=NO, model=NO, history=NO, contact=NO\n";
|
||||
auto accepted = MapText("preprint", preprint + EigenDeck());
|
||||
ASSERT_TRUE(accepted.HasValue());
|
||||
const auto warning = std::find_if(accepted.Value().Warnings().begin(),
|
||||
accepted.Value().Warnings().end(),
|
||||
[](const fesa::Diagnostic& diagnostic) {
|
||||
return diagnostic.keyword == "PREPRINT";
|
||||
});
|
||||
ASSERT_NE(warning, accepted.Value().Warnings().end());
|
||||
EXPECT_EQ(warning->message, "mandatory-eigen-output-unchanged");
|
||||
|
||||
auto rejected =
|
||||
MapText("bad-preprint", "*Preprint, echo=YES\n" + EigenDeck());
|
||||
ASSERT_FALSE(rejected.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(rejected.GetStatus(), "unsupported-keyword"),
|
||||
nullptr);
|
||||
|
||||
auto missing_parameters =
|
||||
MapText("bare-preprint", "*Preprint\n" + EigenDeck());
|
||||
ASSERT_FALSE(missing_parameters.HasValue());
|
||||
EXPECT_NE(
|
||||
FindDiagnostic(missing_parameters.GetStatus(), "unsupported-keyword"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <hdf5.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||
#include "fesa/model/domain.h"
|
||||
#include "io/hdf5/eigen_hdf5_self_check.h"
|
||||
|
||||
namespace {
|
||||
|
||||
fesa::ModelDefinition Definition(const std::filesystem::path& source) {
|
||||
fesa::ModelDefinition definition{};
|
||||
definition.source_path = source;
|
||||
definition.source_content_identity = "fnv1a64:0123456789abcdef";
|
||||
definition.nodes = {{{"I", 1, "1"}, {-1.0, -1.0, 0.0}, {source, 1U}},
|
||||
{{"I", 2, "2"}, {1.0, -1.0, 0.0}, {source, 2U}},
|
||||
{{"I", 3, "3"}, {1.0, 1.0, 0.0}, {source, 3U}},
|
||||
{{"I", 4, "4"}, {-1.0, 1.0, 0.0}, {source, 4U}}};
|
||||
definition.materials = {{"Steel", 12.0, 0.0, {source, 5U}}};
|
||||
definition.shell_sections = {{"S", 1.0, 0U, {source, 6U}}};
|
||||
definition.shell_elements = {{{"I", 1, "1"},
|
||||
fesa::ShellSourceElementType::kS4,
|
||||
{0U, 1U, 2U, 3U},
|
||||
0U,
|
||||
0U,
|
||||
{source, 7U}}};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
definition.shell_node_initial_frames.push_back(
|
||||
{static_cast<fesa::EntityIndex>(node),
|
||||
{0.0, 0.0, 1.0},
|
||||
{1.0, 0.0, 0.0},
|
||||
{0.0, 1.0, 0.0}});
|
||||
}
|
||||
definition.densities = {{0U, 1.0, {source, 8U}}};
|
||||
definition.frequency_step =
|
||||
fesa::FrequencyStepDefinition{"Step-2",
|
||||
"STEP-2",
|
||||
{source, 9U},
|
||||
{source, 10U},
|
||||
{source, 11U},
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
fesa::FrequencyEigensolver::kLanczos,
|
||||
fesa::FrequencyNormalization::kMass,
|
||||
{}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
TEST(EigenHdf5ResultsWriter, WritesModalSchemaAndRejectsTamperedState) {
|
||||
const auto path =
|
||||
std::filesystem::temp_directory_path() / "fesa-eigen-writer-test.h5";
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(path, ignored);
|
||||
const auto source = path.parent_path() / "fesa-eigen-writer-test.inp";
|
||||
auto domain_result = fesa::Domain::Create(Definition(source));
|
||||
ASSERT_TRUE(domain_result.HasValue());
|
||||
fesa::Domain domain{std::move(domain_result.Value())};
|
||||
fesa::EigenHdf5ResultsWriter writer;
|
||||
auto execution = fesa::LinearFrequencyProcedureExecutor{}.Execute(domain);
|
||||
ASSERT_TRUE(execution.HasValue());
|
||||
fesa::EigenAnalysisState state = std::move(execution.Value());
|
||||
ASSERT_TRUE(writer.Write(path, domain, state, {}).IsOk());
|
||||
const hid_t file =
|
||||
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
||||
ASSERT_GE(file, 0);
|
||||
EXPECT_GT(H5Lexists(file, "/steps/Step-1/eigen/modes", H5P_DEFAULT), 0);
|
||||
EXPECT_GE(H5Fclose(file), 0);
|
||||
std::ifstream original_file{path, std::ios::binary};
|
||||
const std::string original{std::istreambuf_iterator<char>{original_file}, {}};
|
||||
original_file.close();
|
||||
const std::vector<std::function<void(fesa::EigenAnalysisState&)>> mutations{
|
||||
[](auto& candidate) {
|
||||
candidate.vectors(0U, 0U) = std::numeric_limits<double>::quiet_NaN();
|
||||
},
|
||||
[](auto& candidate) { candidate.eigenpair_checks[0].rho_b = 1e-5; },
|
||||
[](auto& candidate) { candidate.eigenpair_checks[0].eigenvalue += 1.0; },
|
||||
[](auto& candidate) { candidate.modes[0].cluster_id = 7U; },
|
||||
[](auto& candidate) { candidate.constraint_mask[0] = 1U; },
|
||||
[](auto& candidate) { candidate.nodal_reduction[0].rcond_inf = 1e-15; },
|
||||
[](auto& candidate) { candidate.eta_gram = 1e-5; }};
|
||||
for (const auto& mutate : mutations) {
|
||||
auto candidate = state;
|
||||
mutate(candidate);
|
||||
EXPECT_FALSE(writer.Write(path, domain, candidate, {}).IsOk());
|
||||
}
|
||||
std::ifstream retained_file{path, std::ios::binary};
|
||||
const std::string retained{std::istreambuf_iterator<char>{retained_file}, {}};
|
||||
EXPECT_TRUE(retained == original);
|
||||
retained_file.close();
|
||||
ASSERT_TRUE(
|
||||
fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
|
||||
const hid_t corrupt_file =
|
||||
H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||
ASSERT_GE(corrupt_file, 0);
|
||||
const hid_t nodes = H5Dopen2(corrupt_file, "/model/nodes", H5P_DEFAULT);
|
||||
ASSERT_GE(nodes, 0);
|
||||
const hid_t id_type = H5Tcreate(H5T_COMPOUND, sizeof(std::uint64_t));
|
||||
ASSERT_GE(H5Tinsert(id_type, "internal_node_id", 0U, H5T_NATIVE_UINT64), 0);
|
||||
const std::array<std::uint64_t, 4> duplicate_ids{0U, 0U, 2U, 3U};
|
||||
ASSERT_GE(H5Dwrite(nodes, id_type, H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
||||
duplicate_ids.data()),
|
||||
0);
|
||||
EXPECT_GE(H5Tclose(id_type), 0);
|
||||
EXPECT_GE(H5Dclose(nodes), 0);
|
||||
EXPECT_GE(H5Fclose(corrupt_file), 0);
|
||||
EXPECT_FALSE(
|
||||
fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
|
||||
std::filesystem::remove(path, ignored);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,251 @@
|
||||
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <mkl.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
|
||||
namespace {
|
||||
|
||||
fesa::SparseMatrix MakeDenseCsr(const std::size_t size,
|
||||
const std::vector<double>& values) {
|
||||
EXPECT_EQ(values.size(), size * size);
|
||||
fesa::SparsePattern pattern;
|
||||
std::vector<fesa::CooContribution> contributions;
|
||||
pattern.row_offsets.push_back(0U);
|
||||
for (std::size_t row = 0U; row < size; ++row) {
|
||||
for (std::size_t column = 0U; column < size; ++column) {
|
||||
pattern.column_indices.push_back(column);
|
||||
contributions.push_back(
|
||||
{row, column, values[row * size + column], row, column});
|
||||
}
|
||||
pattern.row_offsets.push_back(pattern.column_indices.size());
|
||||
}
|
||||
auto matrix = fesa::SparseMatrix::FromCoo(size, size,
|
||||
std::move(contributions), pattern);
|
||||
EXPECT_TRUE(matrix.HasValue());
|
||||
return std::move(matrix.Value());
|
||||
}
|
||||
|
||||
void ExpectUnitGram(const fesa::Matrix& vectors) {
|
||||
for (std::size_t left = 0U; left < vectors.Columns(); ++left) {
|
||||
for (std::size_t right = 0U; right < vectors.Columns(); ++right) {
|
||||
double dot = 0.0;
|
||||
for (std::size_t row = 0U; row < vectors.Rows(); ++row) {
|
||||
dot += vectors(row, left) * vectors(row, right);
|
||||
}
|
||||
EXPECT_NEAR(dot, left == right ? 1.0 : 0.0, 1.0e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RequireSuccess(const fesa::Result<fesa::FeastEigenResult>& result) {
|
||||
if (result.HasValue()) return;
|
||||
const auto& diagnostics = result.GetStatus().Diagnostics();
|
||||
ASSERT_FALSE(diagnostics.empty());
|
||||
ADD_FAILURE() << diagnostics.front().code << " "
|
||||
<< diagnostics.front().entity_identity << " "
|
||||
<< diagnostics.front().message;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(MklFeastEigenSolver, SolvesN1AndRetainsBackendEvidence) {
|
||||
const auto matrix = MakeDenseCsr(1U, {4.0});
|
||||
fesa::MklFeastEigenSolver solver;
|
||||
|
||||
const auto result = solver.Solve(matrix, 1U, 1.0);
|
||||
|
||||
RequireSuccess(result);
|
||||
if (!result.HasValue()) return;
|
||||
EXPECT_EQ(result.Value().requested_modes, 1U);
|
||||
ASSERT_EQ(result.Value().eigenvalues.size(), 1U);
|
||||
EXPECT_NEAR(result.Value().eigenvalues[0U], 4.0, 1.0e-10);
|
||||
EXPECT_DOUBLE_EQ(result.Value().operator_scale, 4.0);
|
||||
EXPECT_EQ(result.Value().eigenvectors.Rows(), 1U);
|
||||
EXPECT_EQ(result.Value().eigenvectors.Columns(), 1U);
|
||||
EXPECT_EQ(result.Value().thread_count, 1U);
|
||||
ASSERT_FALSE(result.Value().backend_calls.empty());
|
||||
EXPECT_EQ(result.Value().backend_calls.front().purpose, "total-enclosure");
|
||||
EXPECT_EQ(result.Value().backend_calls.back().kind, "solve");
|
||||
EXPECT_TRUE(result.Value().backend_calls.back().count_available);
|
||||
EXPECT_EQ(result.Value().backend_calls.back().count,
|
||||
result.Value().eigenvalues.size());
|
||||
EXPECT_TRUE(result.Value().backend_calls.back().loop_available);
|
||||
EXPECT_TRUE(result.Value().backend_calls.back().epsout_available);
|
||||
EXPECT_EQ(result.Value().backend_calls.back().outcome, "accepted");
|
||||
EXPECT_EQ(result.Value().backend_calls.back().decision_reason, "accepted");
|
||||
EXPECT_FALSE(result.Value().upper_search_termination.empty());
|
||||
ExpectUnitGram(result.Value().eigenvectors);
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, SolvesAllZeroWithExplicitPhysicalScale) {
|
||||
const auto matrix = MakeDenseCsr(2U, {0.0, 0.0, 0.0, 0.0});
|
||||
fesa::MklFeastEigenSolver solver;
|
||||
|
||||
const auto result = solver.Solve(matrix, 2U, 7.0);
|
||||
|
||||
RequireSuccess(result);
|
||||
if (!result.HasValue()) return;
|
||||
ASSERT_EQ(result.Value().eigenvalues.size(), 2U);
|
||||
EXPECT_DOUBLE_EQ(result.Value().operator_scale, 7.0);
|
||||
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[0U], 0.0);
|
||||
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[1U], 0.0);
|
||||
ExpectUnitGram(result.Value().eigenvectors);
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, PreservesRepeatedMultiplicityAndStableOrder) {
|
||||
const auto matrix =
|
||||
MakeDenseCsr(3U, {2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 5.0});
|
||||
fesa::MklFeastEigenSolver solver;
|
||||
|
||||
const auto first = solver.Solve(matrix, 3U, 1.0);
|
||||
const auto second = solver.Solve(matrix, 3U, 1.0);
|
||||
|
||||
RequireSuccess(first);
|
||||
RequireSuccess(second);
|
||||
if (!first.HasValue() || !second.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);
|
||||
ExpectUnitGram(first.Value().eigenvectors);
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, UsesGuardedPrefixWithoutDroppingRepeatedCluster) {
|
||||
constexpr std::size_t kSize = 64U;
|
||||
std::vector<double> values(kSize * kSize, 0.0);
|
||||
for (std::size_t index = 0U; index < kSize; ++index) {
|
||||
values[index * kSize + index] = index < 40U ? 1.0 : index - 38U;
|
||||
}
|
||||
const auto matrix = MakeDenseCsr(kSize, values);
|
||||
fesa::MklFeastEigenSolver solver;
|
||||
|
||||
const auto one_mode = solver.Solve(matrix, 1U, 1.0);
|
||||
const auto forty_first = solver.Solve(matrix, 41U, 1.0);
|
||||
|
||||
RequireSuccess(one_mode);
|
||||
RequireSuccess(forty_first);
|
||||
if (!one_mode.HasValue() || !forty_first.HasValue()) return;
|
||||
EXPECT_GE(one_mode.Value().eigenvalues.size(), 40U);
|
||||
EXPECT_GE(forty_first.Value().eigenvalues.size(), 41U);
|
||||
for (std::size_t index = 0U; index < 40U; ++index) {
|
||||
EXPECT_NEAR(one_mode.Value().eigenvalues[index], 1.0, 1.0e-10);
|
||||
}
|
||||
EXPECT_NEAR(forty_first.Value().eigenvalues[40U], 2.0, 1.0e-10);
|
||||
EXPECT_EQ(one_mode.Value().backend_calls.back().kind, "solve");
|
||||
EXPECT_EQ(forty_first.Value().backend_calls.back().kind, "solve");
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, RejectsSignificantNegativeSpectrum) {
|
||||
const auto matrix = MakeDenseCsr(1U, {-2.0});
|
||||
fesa::MklFeastEigenSolver solver;
|
||||
|
||||
const auto result = solver.Solve(matrix, 1U, 1.0);
|
||||
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kSolver);
|
||||
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U);
|
||||
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
|
||||
"negative-eigenvalue-spectrum");
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, KeepsCloseDistinctModesSeparated) {
|
||||
const auto matrix = MakeDenseCsr(2U, {1.0, 0.0, 0.0, 1.001});
|
||||
fesa::MklFeastEigenSolver solver;
|
||||
|
||||
const auto result = solver.Solve(matrix, 2U, 1.0);
|
||||
|
||||
RequireSuccess(result);
|
||||
if (!result.HasValue()) return;
|
||||
ASSERT_EQ(result.Value().eigenvalues.size(), 2U);
|
||||
EXPECT_NEAR(result.Value().eigenvalues[0U], 1.0, 1.0e-10);
|
||||
EXPECT_NEAR(result.Value().eigenvalues[1U], 1.001, 1.0e-10);
|
||||
EXPECT_NEAR(std::abs(result.Value().eigenvectors(0U, 0U)), 1.0, 1.0e-10);
|
||||
EXPECT_NEAR(std::abs(result.Value().eigenvectors(1U, 1U)), 1.0, 1.0e-10);
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, RestoresCallingThreadLocalSetting) {
|
||||
const auto matrix = MakeDenseCsr(1U, {4.0});
|
||||
const int prior = mkl_set_num_threads_local(2);
|
||||
fesa::MklFeastEigenSolver solver;
|
||||
|
||||
const auto result = solver.Solve(matrix, 1U, 1.0);
|
||||
const int restored = mkl_set_num_threads_local(2);
|
||||
static_cast<void>(mkl_set_num_threads_local(prior));
|
||||
|
||||
RequireSuccess(result);
|
||||
if (!result.HasValue()) return;
|
||||
EXPECT_EQ(restored, 2);
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, RejectsInvalidRequestAndZeroScale) {
|
||||
const auto matrix = MakeDenseCsr(1U, {0.0});
|
||||
fesa::MklFeastEigenSolver solver;
|
||||
|
||||
const auto zero_request = solver.Solve(matrix, 0U, 1.0);
|
||||
ASSERT_FALSE(zero_request.HasValue());
|
||||
EXPECT_EQ(zero_request.GetStatus().Diagnostics()[0U].code,
|
||||
"feast-invalid-dimension");
|
||||
const auto invalid_scale = solver.Solve(matrix, 1U, 0.0);
|
||||
ASSERT_FALSE(invalid_scale.HasValue());
|
||||
EXPECT_EQ(invalid_scale.GetStatus().Diagnostics()[0U].code,
|
||||
"feast-invalid-zero-scale");
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, PreservesLowestPrefixForZeroAndPositiveSpectra) {
|
||||
const std::vector<std::vector<double>> spectra{{2.0},
|
||||
{0.0, 0.0, 0.0, 2.0, 5.0, 9.0},
|
||||
{1.0, 2.0, 4.0, 8.0},
|
||||
{0.0, 0.0, 0.0, 0.0}};
|
||||
const std::vector<std::vector<std::size_t>> requests{
|
||||
{1U}, {1U, 4U, 6U}, {1U, 2U, 4U}, {1U, 4U}};
|
||||
const fesa::MklFeastEigenSolver solver;
|
||||
for (std::size_t case_index = 0U; case_index < spectra.size(); ++case_index) {
|
||||
const auto& spectrum = spectra[case_index];
|
||||
const auto n = spectrum.size();
|
||||
std::vector<double> values(n * n, 0.0);
|
||||
for (std::size_t i = 0U; i < n; ++i) values[i * n + i] = spectrum[i];
|
||||
const auto matrix = MakeDenseCsr(n, values);
|
||||
for (const auto requested : requests[case_index]) {
|
||||
const auto result = solver.Solve(matrix, requested, 1.0);
|
||||
RequireSuccess(result);
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
ASSERT_GE(result.Value().eigenvalues.size(), requested);
|
||||
for (std::size_t i = 0U; i < requested; ++i)
|
||||
EXPECT_NEAR(result.Value().eigenvalues[i], spectrum[i], 1e-10);
|
||||
ExpectUnitGram(result.Value().eigenvectors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MklFeastEigenSolver, ResolvesCloseDistinctSpectrumAfterGivensCongruence) {
|
||||
const std::array<double, 4> spectrum{1.0, 1.0 + std::ldexp(1.0, -28),
|
||||
1.0 + std::ldexp(1.0, -27), 8.0};
|
||||
const std::array<std::array<double, 4>, 4> q{{{{0.6, 0.0, 0.0, -0.8}},
|
||||
{{0.0, 0.6, -0.8, 0.0}},
|
||||
{{0.0, 0.8, 0.6, 0.0}},
|
||||
{{0.8, 0.0, 0.0, 0.6}}}};
|
||||
std::vector<double> values(16U, 0.0);
|
||||
for (std::size_t i = 0U; i < 4U; ++i)
|
||||
for (std::size_t j = 0U; j < 4U; ++j)
|
||||
for (std::size_t k = 0U; k < 4U; ++k)
|
||||
values[4U * i + j] += q[i][k] * spectrum[k] * q[j][k];
|
||||
const auto result =
|
||||
fesa::MklFeastEigenSolver{}.Solve(MakeDenseCsr(4U, values), 2U, 1.0);
|
||||
RequireSuccess(result);
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
ASSERT_EQ(result.Value().eigenvalues.size(), 4U);
|
||||
for (std::size_t i = 0U; i < 4U; ++i)
|
||||
EXPECT_NEAR(result.Value().eigenvalues[i], spectrum[i], 1e-12);
|
||||
EXPECT_GT(result.Value().eigenvalues[1U] - result.Value().eigenvalues[0U],
|
||||
1e-9);
|
||||
ExpectUnitGram(result.Value().eigenvectors);
|
||||
}
|
||||
Reference in New Issue
Block a user