#include "fesa/analysis/linear_frequency_procedure.h" #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/prepared_analysis.h" #include "fesa/elements/element_factory.h" #include "fesa/elements/mitc4_shell.h" #include "fesa/model/domain.h" namespace { std::string StatusText(const fesa::Status& status) { std::string result; for (const auto& diagnostic : status.Diagnostics()) result += diagnostic.code + " " + diagnostic.message + "\n"; return result; } fesa::Result SharedNodeDomain(const bool reverse_source_order) { std::ostringstream deck; deck << R"inp(*Part,name=Panel *Node 1,0,0,0 2,1,0,0 3,2,0,0 4,0,1,0 5,1,1,0 6,2,1,0 *Element,type=S4 )inp"; if (reverse_source_order) deck << "20,2,3,6,5\n10,1,2,5,4\n"; else deck << "10,1,2,5,4\n20,2,3,6,5\n"; deck << R"inp(*Elset,elset=Thin 10 *Elset,elset=Thick 20 *Shell Section,elset=Thin,material=Light 0.5 *Shell Section,elset=Thick,material=Heavy 1.0 *End Part *Assembly,name=Assembly *Instance,name=Panel-1,part=Panel *End Instance *Nset,nset=Fixed,instance=Panel-1 1,2,3,4,6 *Nset,nset=Shared,instance=Panel-1 5 *End Assembly *Material,name=Light *Elastic 12,0 *Density 2 *Material,name=Heavy *Elastic 12,0 *Density 5 *Boundary Fixed,1,6 Shared,1,3 Shared,5,6 *Step,name=Step-2,nlgeom=NO,perturbation *Frequency 1 *End Step )inp"; static std::atomic sequence{0U}; const auto path = std::filesystem::temp_directory_path() / ("fesa-eigen-mass-" + std::to_string(std::chrono::steady_clock::now() .time_since_epoch() .count()) + "-" + std::to_string(sequence.fetch_add(1U)) + ".inp"); { std::ofstream stream{path}; stream << deck.str(); } auto prepared = fesa::PrepareAnalysisInput(path); std::error_code ignored; std::filesystem::remove(path, ignored); if (!prepared.HasValue()) return fesa::Result::Failure(prepared.GetStatus()); return fesa::Result::Success( std::move(prepared.Value().domain)); } struct SharedDofPencil { double stiffness{0.0}; double mass{0.0}; }; fesa::Result DirectSharedDofPencil( const fesa::Domain& domain) { const fesa::ElementFactory factory; SharedDofPencil result; for (std::size_t index = 0U; index < domain.ShellElements().Size(); ++index) { const auto& definition = domain.ShellElements()[index]; const auto runtime = factory.Create(definition, domain); if (!runtime.HasValue()) return fesa::Result::Failure(runtime.GetStatus()); const auto* shell = dynamic_cast(runtime.Value().get()); if (shell == nullptr) return fesa::Result::Failure(fesa::Status::Failure( fesa::FailureCategory::kModel, {{fesa::Severity::kError, "invalid-test-shell", {}, "ELEMENT", {}, "Frequency mass fixture did not create an MITC4 shell."}})); const auto local = std::find(definition.NodeIndices().begin(), definition.NodeIndices().end(), 4U); if (local == definition.NodeIndices().end()) return fesa::Result::Failure(fesa::Status::Failure( fesa::FailureCategory::kModel, {{fesa::Severity::kError, "invalid-test-shell", {}, "ELEMENT", {}, "Frequency mass fixture lost its shared node."}})); const auto mass = shell->Mass(definition.MaterialIndex() == 0U ? 2.0 : 5.0); const auto stiffness = shell->Stiffness(); if (!mass.HasValue()) return fesa::Result::Failure(mass.GetStatus()); if (!stiffness.HasValue()) return fesa::Result::Failure(stiffness.GetStatus()); const std::size_t dof = 6U * static_cast( std::distance(definition.NodeIndices().begin(), local)) + 3U; result.mass += mass.Value().lumped_global24(dof, dof); result.stiffness += stiffness.Value().stabilized_global24(dof, dof); } return fesa::Result::Success(result); } // E2-C06: sections and densities differ across the shared source node. The // one free physical rotation makes the executor's reduced pencil exactly the // independently scattered local diagonal sum. TEST(EigenMassAssembly, ScattersHeterogeneousSharedNodeMassInSourceOrder) { constexpr double analytic_shared_rotary_mass = 7.0 / 64.0; std::array eigenvalues{}; for (std::size_t order = 0U; order < eigenvalues.size(); ++order) { const auto domain = SharedNodeDomain(order != 0U); ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus()); const auto expected = DirectSharedDofPencil(domain.Value()); ASSERT_TRUE(expected.HasValue()) << StatusText(expected.GetStatus()); EXPECT_LE(std::abs(expected.Value().mass - analytic_shared_rotary_mass) / analytic_shared_rotary_mass, 1e-12); const auto state = fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value()); ASSERT_TRUE(state.HasValue()) << StatusText(state.GetStatus()); ASSERT_EQ(state.Value().free_dof_count, 1U); ASSERT_EQ(state.Value().modes.size(), 1U); const double expected_eigenvalue = expected.Value().stiffness / analytic_shared_rotary_mass; EXPECT_LE(std::abs(state.Value().modes[0U].eigenvalue - expected_eigenvalue) / (std::abs(state.Value().modes[0U].eigenvalue) + std::abs(expected_eigenvalue)), 1e-12); const double amplitude = state.Value().vectors(0U, 6U * 4U + 3U); EXPECT_LE(std::abs(amplitude * amplitude * analytic_shared_rotary_mass - 1.0), 1e-12); eigenvalues[order] = state.Value().modes[0U].eigenvalue; } EXPECT_NEAR(eigenvalues[0U], eigenvalues[1U], 1e-12); } } // namespace