#include "fesa/analysis/solid_frequency_procedure.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" #include "fesa/model/domain.h" #include "solvers/eigen/mkl_feast_eigen_solver_test_seam.h" namespace { std::string StatusText(const fesa::Status& status) { std::string text; for (const auto& d : status.Diagnostics()) text += d.code + " " + d.message; return text; } fesa::Result Brick(std::size_t requested = 6U, const std::string& boundary = "", double length = 1.0, double rho = 2.0, double modulus = 120.0) { constexpr int signs[8][3] = {{-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, {-1, 1, -1}, {-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}}; std::ostringstream deck; deck.precision(17); deck << "*Part,name=P\n*Node\n"; for (std::size_t a = 0; a < 8; ++a) deck << (a + 1) * 10 << ',' << length * signs[a][0] << ',' << 2 * length * signs[a][1] << ',' << 3 * length * signs[a][2] << '\n'; deck << "*Element,type=C3D8,elset=E\n99,10,20,30,40,50,60,70,80\n" "*Solid Section,elset=E,material=M\n,\n*End Part\n" "*Assembly,name=A\n*Instance,name=I,part=P\n*End Instance\n" "*End Assembly\n*Material,name=M\n*Elastic\n" << modulus << ",.25\n"; if (rho != 0.0) deck << "*Density\n" << rho << '\n'; deck << "*Step,name=Modes\n*Frequency\n" << requested << '\n'; if (!boundary.empty()) deck << "*Boundary\n" << boundary; deck << "*End Step\n"; static std::atomic serial{0}; const auto path = std::filesystem::temp_directory_path() / ("fesa-solid-frequency-" + std::to_string(serial++) + ".inp"); { std::ofstream out(path); out << deck.str(); } auto syntax = fesa::AbaqusInputReader{}.Read(path); std::filesystem::remove(path); if (!syntax.HasValue()) return fesa::Result::Failure(syntax.GetStatus()); return fesa::AbaqusDomainMapper{}.Map(syntax.Value()); } } // namespace TEST(SolidFrequencyProcedure, FreeBrickRetainsSixRigidModes) { auto domain = Brick(); ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus()); const auto result = fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus()); ASSERT_EQ(result.Value().modes.size(), 6U); for (const auto& mode : result.Value().modes) EXPECT_EQ(mode.classification, 0U); EXPECT_EQ(result.Value().vectors.Rows(), 6U); EXPECT_EQ(result.Value().vectors.Columns(), 24U); } namespace { constexpr int kSigns[8][3] = {{-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, {-1, 1, -1}, {-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}}; // Exact tensor-polynomial integrals, independent of production Gauss assembly. double GradientIntegral(std::size_t a, std::size_t b, std::size_t i, std::size_t j) { double integral = 48.0 * kSigns[a][i] * kSigns[b][j] / (64.0 * static_cast((i + 1U) * (j + 1U))); for (std::size_t d = 0; d < 3; ++d) if (d != i && d != j) integral *= 1.0 + kSigns[a][d] * kSigns[b][d] / 3.0; return integral; } std::vector DenseOracle(const std::vector& free) { const auto n = free.size(); std::vector k(n * n, 0.0), m(n * n, 0.0), eigen(n, 0.0); for (std::size_t row = 0; row < n; ++row) for (std::size_t col = 0; col < n; ++col) { const auto a = free[row] / 3U, b = free[col] / 3U; const auto i = free[row] % 3U, j = free[col] % 3U; double gradient_dot = 0.0; for (std::size_t d = 0; d < 3; ++d) gradient_dot += GradientIntegral(a, b, d, d); k[row * n + col] = 48.0 * (GradientIntegral(a, b, i, j) + GradientIntegral(a, b, j, i) + (i == j ? gradient_dot : 0.0)); if (i == j) { m[row * n + col] = 2.0 * 48.0 / 64.0; for (std::size_t d = 0; d < 3; ++d) m[row * n + col] *= 1.0 + kSigns[a][d] * kSigns[b][d] / 3.0; } } EXPECT_EQ( LAPACKE_dsygv(LAPACK_ROW_MAJOR, 1, 'N', 'U', static_cast(n), k.data(), static_cast(n), m.data(), static_cast(n), eigen.data()), 0); return eigen; } const std::string kFaceFixed = "10,1,3\n40,1,3\n50,1,3\n80,1,3\n"; std::vector FaceFree() { std::vector free; for (const auto node : {1U, 2U, 5U, 6U}) for (std::size_t d = 0; d < 3; ++d) free.push_back(3U * node + d); return free; } } // namespace TEST(SolidFrequencyProcedure, OriginalConsistentPencilMatchesIndependentDenseOracle) { for (const bool constrained : {false, true}) { const std::size_t count = constrained ? 12U : 24U; auto domain = Brick(count, constrained ? kFaceFixed : ""); ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus()); auto result = fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus()); std::vector free; if (constrained) free = FaceFree(); else for (std::size_t i = 0; i < 24; ++i) free.push_back(i); const auto oracle = DenseOracle(free); const auto& state = result.Value(); ASSERT_EQ(state.modes.size(), oracle.size()); ASSERT_EQ(state.eigenpair_checks.size(), oracle.size()); for (std::size_t c = 0; c < oracle.size(); ++c) { EXPECT_NEAR(state.modes[c].eigenvalue, oracle[c], 1e-10 * state.classification_scale); EXPECT_EQ(state.modes[c].mode_id, c + 1); EXPECT_EQ(state.eigenpair_checks[c].sorted_rank, c + 1); EXPECT_LE(state.eigenpair_checks[c].eta_r, 1e-10); EXPECT_LE(state.eigenpair_checks[c].eta_energy, 1e-10); EXPECT_LE(state.eigenpair_checks[c].eta_norm, 1e-10); EXPECT_NEAR(state.eigenpair_checks[c].modal_mass, 1.0, 1e-10); std::size_t pivot = 0; for (std::size_t row = 0; row < 24; ++row) { if (std::abs(state.vectors(c, row)) > std::abs(state.vectors(c, pivot))) pivot = row; if (state.constraint_mask[row]) EXPECT_EQ(state.vectors(c, row), 0.0); } EXPECT_GT(state.vectors(c, pivot), 0.0); } EXPECT_LE(state.eta_gram, 1e-10); EXPECT_LE(state.max_eta_orth, 1e-10); EXPECT_NEAR(state.mass_ideal_lower_bound, 12.0 / 27.0, 1e-13); EXPECT_GT(state.mass_lower_bound, 0.0); EXPECT_LT(state.mass_lower_bound, state.mass_ideal_lower_bound); EXPECT_GT(state.mass_roundoff_guard, 0.0); EXPECT_EQ(state.backend.integer_abi, "ILP64"); EXPECT_EQ(state.backend.eigenvectors.Rows(), 0U); EXPECT_EQ(state.backend.backend_calls.front().kind, "mass-spd"); } } TEST(SolidFrequencyProcedure, DensityLengthAndMaterialScaling) { const auto base_domain = Brick(4U, kFaceFixed); ASSERT_TRUE(base_domain.HasValue()); const auto base = fesa::SolidFrequencyProcedureExecutor{}.Execute(base_domain.Value()); ASSERT_TRUE(base.HasValue()) << StatusText(base.GetStatus()); for (const auto parameters : {std::array{1, 8, 120, .25}, std::array{2, 2, 120, .25}, std::array{1, 2, 480, 4}}) { const auto domain = Brick(4U, kFaceFixed, parameters[0], parameters[1], parameters[2]); ASSERT_TRUE(domain.HasValue()); const auto result = fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus()); for (std::size_t c = 0; c < 4; ++c) EXPECT_NEAR(result.Value().modes[c].eigenvalue, base.Value().modes[c].eigenvalue * parameters[3], 1e-10 * result.Value().classification_scale); } } TEST(SolidFrequencyProcedure, OneFreeDofAndAllConstrainedAndExcessRequest) { const std::string one = "10,1,2\n20,1,3\n30,1,3\n40,1,3\n50,1,3\n60,1,3\n70,1,3\n80,1,3\n"; auto domain = Brick(1U, one); ASSERT_TRUE(domain.HasValue()); auto result = fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus()); EXPECT_NEAR(result.Value().modes[0].eigenvalue, DenseOracle({2U})[0], 1e-11); const std::string all = "10,1,3\n20,1,3\n30,1,3\n40,1,3\n50,1,3\n60,1,3\n70,1,3\n80,1,3\n"; for (const auto pair : {std::pair{1U, all}, {13U, kFaceFixed}}) { auto invalid = Brick(pair.first, pair.second); ASSERT_TRUE(invalid.HasValue()); EXPECT_FALSE(fesa::SolidFrequencyProcedureExecutor{} .Execute(invalid.Value()) .HasValue()); } } TEST(SolidFrequencyProcedure, MissingDensityAndFailedBackendPreservePriorCandidate) { auto missing = Brick(1U, kFaceFixed, 1.0, 0.0); if (missing.HasValue()) EXPECT_FALSE(fesa::SolidFrequencyProcedureExecutor{} .Execute(missing.Value()) .HasValue()); auto domain = Brick(4U, kFaceFixed); ASSERT_TRUE(domain.HasValue()); const auto good = fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(good.HasValue()); const auto previous = good.Value().modes[0].eigenvalue; { fesa::internal::ScopedFeastBackendTestHook hook{ [](const auto& call, auto& output) { if (!call.count) output.eigenvalues.back() = 9999.0; }}; EXPECT_FALSE(fesa::SolidFrequencyProcedureExecutor{} .Execute(domain.Value()) .HasValue()); } EXPECT_EQ(good.Value().modes[0].eigenvalue, previous); const auto again = fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(again.HasValue()); EXPECT_NEAR(again.Value().modes[0].eigenvalue, previous, 1e-12); } TEST(SolidFrequencyProcedure, NearZeroNegativeRemainsRawWithoutFrequency) { auto domain = Brick(); ASSERT_TRUE(domain.HasValue()); fesa::internal::ScopedFeastBackendTestHook hook{ [](const auto& call, auto& output) { if (!call.count) output.eigenvalues[0] = -1e-12; }}; const auto result = fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus()); EXPECT_LT(result.Value().modes[0].eigenvalue, 0.0); EXPECT_EQ(result.Value().modes[0].classification, 0U); EXPECT_FALSE(result.Value().modes[0].frequency.has_value()); for (const auto& mode : result.Value().modes) EXPECT_EQ(mode.cluster_id, 1U); } TEST(SolidFrequencyProcedure, SuppliedFrequencyInputHasIndependentNumericalEvidence) { auto root = std::filesystem::path{__FILE__}.parent_path(); while (!root.empty() && !std::filesystem::exists(root / "CMakePresets.json")) root = root.parent_path(); ASSERT_FALSE(root.empty()); const auto syntax = fesa::AbaqusInputReader{}.Read(root / "reference/hexa/hexa_freq.inp"); ASSERT_TRUE(syntax.HasValue()) << StatusText(syntax.GetStatus()); const auto domain = fesa::AbaqusDomainMapper{}.Map(syntax.Value()); ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus()); const auto result = fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus()); EXPECT_EQ(result.Value().modes.size(), 10U); EXPECT_EQ(result.Value().vectors.Columns(), 3U * 442U); EXPECT_GE(result.Value().eigenpair_checks.size(), 10U); EXPECT_GT(result.Value().mass_lower_bound, 0.0); EXPECT_LE(result.Value().eta_gram, 1e-10); EXPECT_LE(result.Value().max_eta_orth, 1e-10); // Original modal Value output is pending. This proves only our independent // checks on the supplied input; it is not an external frequency comparison. }