#include "fesa/elements/element_factory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/elements/element.h" #include "fesa/elements/euler_beam_3d.h" #include "fesa/elements/mitc4_shell.h" #include "fesa/fem/dof_manager.h" #include "fesa/model/domain.h" namespace { fesa::ModelDefinition BeamDefinition( std::array node_indices = {0U, 1U}, fesa::EntityIndex material_index = 0U, fesa::EntityIndex property_index = 0U) { fesa::ModelDefinition definition{}; definition.source_path = "element-factory-beam.inp"; definition.source_content_identity = "fnv1a64:beam"; definition.nodes = { {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {definition.source_path, 1U}}, {{"Beam-1", 2, "2"}, {2.0, 0.0, 0.0}, {definition.source_path, 2U}}}; definition.materials = { {"Steel", 100.0, 0.25, {definition.source_path, 10U}}}; definition.sections = {{"BeamSection", 2.0, 1.0, 0.0, 1.0, 1.0, {0.0, 1.0, 0.0}, {}, {definition.source_path, 20U}}}; definition.elements = {{{"Beam-1", 7, "7"}, node_indices, material_index, property_index, {definition.source_path, 30U}}}; return definition; } fesa::ModelDefinition ShellDefinition( fesa::EntityIndex definition_material_index = 0U, fesa::EntityIndex section_material_index = 0U) { fesa::ModelDefinition definition{}; definition.source_path = "element-factory-shell.inp"; definition.source_content_identity = "fnv1a64:shell"; definition.nodes = { {{"Shell-1", 1, "1"}, {-1.0, -1.0, 0.0}, {definition.source_path, 1U}}, {{"Shell-1", 2, "2"}, {1.0, -1.0, 0.0}, {definition.source_path, 2U}}, {{"Shell-1", 3, "3"}, {1.0, 1.0, 0.0}, {definition.source_path, 3U}}, {{"Shell-1", 4, "4"}, {-1.0, 1.0, 0.0}, {definition.source_path, 4U}}}; definition.materials = { {"Steel-A", 120.0, 0.25, {definition.source_path, 10U}}, {"Steel-B", 200.0, 0.30, {definition.source_path, 11U}}}; definition.shell_sections = {{"ShellSection", 0.2, section_material_index, {definition.source_path, 20U}}}; definition.shell_elements = {{{"Shell-1", 9, "9"}, fesa::ShellSourceElementType::kS4, {0U, 1U, 2U, 3U}, definition_material_index, 0U, {definition.source_path, 30U}}}; for (fesa::EntityIndex node = 0U; node < 4U; ++node) { definition.shell_node_initial_frames.push_back( {node, {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}}); } return definition; } fesa::Domain MakeDomain(fesa::ModelDefinition definition) { auto candidate = fesa::Domain::Create(std::move(definition)); if (!candidate.HasValue()) { throw std::runtime_error{"Expected a valid Domain fixture."}; } return std::move(candidate.Value()); } void ExpectSameMatrix(const fesa::Matrix& actual, const fesa::Matrix& expected) { ASSERT_EQ(actual.Rows(), expected.Rows()); ASSERT_EQ(actual.Columns(), expected.Columns()); for (std::size_t row = 0U; row < actual.Rows(); ++row) { for (std::size_t column = 0U; column < actual.Columns(); ++column) { EXPECT_DOUBLE_EQ(actual(row, column), expected(row, column)); } } } void ExpectModelFailure( const fesa::Result>& candidate) { ASSERT_FALSE(candidate.HasValue()); EXPECT_EQ(candidate.GetStatus().Category(), fesa::FailureCategory::kModel); ASSERT_EQ(candidate.GetStatus().Diagnostics().size(), 1U); EXPECT_EQ(candidate.GetStatus().Diagnostics()[0].severity, fesa::Severity::kError); } class UnknownElementDefinition final : public fesa::ElementDefinition { public: fesa::ElementDefinitionKind Kind() const noexcept override { return static_cast(99); } const fesa::SourceEntityId& SourceId() const noexcept override { return source_id_; } std::string_view SourceElementType() const noexcept override { return "UNKNOWN"; } const std::vector& NodeIndices() const noexcept override { return node_indices_; } fesa::EntityIndex MaterialIndex() const noexcept override { return 0U; } fesa::EntityIndex PropertyIndex() const noexcept override { return 0U; } private: fesa::SourceEntityId source_id_{"Unknown-1", 99, "99"}; std::vector node_indices_{0U, 1U}; }; class DestructionProbeElement final : public fesa::Element { public: explicit DestructionProbeElement(bool& destroyed) : destroyed_{&destroyed} {} ~DestructionProbeElement() override { *destroyed_ = true; } const fesa::ElementDofLayout& DofLayout() const noexcept override { return layout_; } fesa::Result ComputeStiffness() const override { return fesa::Result::Success( {layout_, fesa::Matrix{0U, 0U}}); } fesa::Result Recover( const fesa::Vector&) const override { return fesa::Result::Success( {layout_.source_id, fesa::BeamElementResultRows{}}); } private: bool* destroyed_; fesa::ElementDofLayout layout_{}; }; } // namespace // C-ELEMENT-001 TEST(ElementFactory, CreatesBeamWithStableLayoutStiffnessAndRecovery) { static_assert(std::has_virtual_destructor_v); bool destroyed = false; { std::unique_ptr probe = std::make_unique(destroyed); EXPECT_NE(probe, nullptr); } EXPECT_TRUE(destroyed); const fesa::Domain domain = MakeDomain(BeamDefinition()); const fesa::ElementFactory factory{}; auto candidate = factory.Create(domain.Elements()[0U], domain); ASSERT_TRUE(candidate.HasValue()); ASSERT_NE(candidate.Value(), nullptr); const std::unique_ptr& element = candidate.Value(); const auto& layout = element->DofLayout(); EXPECT_EQ(layout.source_id.source_label_text, "7"); EXPECT_EQ(layout.node_indices, (std::vector{0U, 1U})); EXPECT_EQ(layout.components_per_node, (std::vector{ fesa::DofComponent::kUx, fesa::DofComponent::kUy, fesa::DofComponent::kUz, fesa::DofComponent::kUrx, fesa::DofComponent::kUry, fesa::DofComponent::kUrz})); auto stiffness = element->ComputeStiffness(); ASSERT_TRUE(stiffness.HasValue()); EXPECT_EQ(stiffness.Value().layout.node_indices, layout.node_indices); auto direct = fesa::EulerBeam3D::Create( domain.Nodes()[0U], domain.Nodes()[1U], domain.Sections()[0U], domain.LinearElasticMaterials()[0U]); ASSERT_TRUE(direct.HasValue()); ExpectSameMatrix(stiffness.Value().values, direct.Value().GlobalStiffness()); fesa::Vector displacement{12U}; displacement[6U] = 0.1; auto result = element->Recover(displacement); ASSERT_TRUE(result.HasValue()); EXPECT_EQ(result.Value().source_id.source_label_text, "7"); ASSERT_TRUE(std::holds_alternative( result.Value().payload)); const auto& rows = std::get(result.Value().payload); ASSERT_EQ(rows.endpoint_rows.size(), 2U); EXPECT_DOUBLE_EQ(rows.endpoint_rows[0U].end_action[0U], -10.0); EXPECT_DOUBLE_EQ(rows.endpoint_rows[1U].end_action[0U], 10.0); EXPECT_DOUBLE_EQ(rows.endpoint_rows[0U].section_resultant[0U], 10.0); EXPECT_EQ(rows.endpoint_rows[0U].node.source_label_text, "1"); EXPECT_EQ(rows.endpoint_rows[1U].node.source_label_text, "2"); EXPECT_EQ(rows.gauss_rows.size(), 2U); EXPECT_EQ(rows.stress_rows.size(), 2U); } TEST(ElementFactory, CreatesMitc4WithStableLayoutStiffnessAndRecovery) { auto mixed_definition = ShellDefinition(); mixed_definition.sections = {{"BeamSection", 2.0, 1.0, 0.0, 1.0, 1.0, {0.0, 1.0, 0.0}, {}, {mixed_definition.source_path, 21U}}}; mixed_definition.elements = {{{"Shell-1", 8, "8"}, {0U, 1U}, 0U, 0U, {mixed_definition.source_path, 29U}}}; const fesa::Domain domain = MakeDomain(std::move(mixed_definition)); const fesa::ElementFactory factory{}; ASSERT_EQ(domain.Elements().Size(), 2U); auto candidate = factory.Create(domain.Elements()[1U], domain); ASSERT_TRUE(candidate.HasValue()); ASSERT_NE(candidate.Value(), nullptr); const std::unique_ptr& element = candidate.Value(); EXPECT_EQ(element->DofLayout().node_indices, (std::vector{0U, 1U, 2U, 3U})); EXPECT_EQ(element->DofLayout().components_per_node.size(), 6U); auto stiffness = element->ComputeStiffness(); ASSERT_TRUE(stiffness.HasValue()); std::array nodes{ &domain.Nodes()[0U], &domain.Nodes()[1U], &domain.Nodes()[2U], &domain.Nodes()[3U]}; std::array, 4> directors{ std::array{0.0, 0.0, 1.0}, {0.0, 0.0, 1.0}, {0.0, 0.0, 1.0}, {0.0, 0.0, 1.0}}; auto direct = fesa::Mitc4Shell::Create(nodes, directors, domain.ShellSections()[0U], domain.LinearElasticMaterials()[0U]); ASSERT_TRUE(direct.HasValue()); auto direct_stiffness = direct.Value().Stiffness(); ASSERT_TRUE(direct_stiffness.HasValue()); ExpectSameMatrix(stiffness.Value().values, direct_stiffness.Value().stabilized_global24); auto result = element->Recover(fesa::Vector{24U}); ASSERT_TRUE(result.HasValue()); ASSERT_TRUE(std::holds_alternative( result.Value().payload)); const auto& rows = std::get(result.Value().payload); ASSERT_EQ(rows.rows.size(), 4U); EXPECT_DOUBLE_EQ(rows.physical_strain_energy, 0.0); EXPECT_EQ(rows.rows[0U].location, fesa::ShellMidsurfaceLocation::kGp1); EXPECT_EQ(rows.rows[3U].location, fesa::ShellMidsurfaceLocation::kGp4); for (const auto& row : rows.rows) { EXPECT_EQ(row.element, fesa::EntityIndex{1U}); } } TEST(ElementFactory, OwnerBuildsNonOwningViewInStableElementOrder) { const fesa::Domain beam_domain = MakeDomain(BeamDefinition()); const fesa::Domain shell_domain = MakeDomain(ShellDefinition()); const fesa::ElementFactory factory{}; auto beam = factory.Create(beam_domain.Elements()[0U], beam_domain); auto shell = factory.Create(shell_domain.Elements()[0U], shell_domain); ASSERT_TRUE(beam.HasValue()); ASSERT_TRUE(shell.HasValue()); std::vector> owner; owner.push_back(std::move(beam.Value())); owner.push_back(std::move(shell.Value())); fesa::ElementView view; view.reserve(owner.size()); for (const auto& element : owner) { view.push_back(std::cref(*element)); } ASSERT_EQ(view.size(), 2U); EXPECT_EQ(&view[0U].get(), owner[0U].get()); EXPECT_EQ(&view[1U].get(), owner[1U].get()); EXPECT_EQ(view[0U].get().DofLayout().source_id.source_label_text, "7"); EXPECT_EQ(view[1U].get().DofLayout().source_id.source_label_text, "9"); } TEST(ElementFactory, RejectsUnknownAndIncompatibleDefinitions) { const fesa::ElementFactory factory{}; const fesa::Domain beam_domain = MakeDomain(BeamDefinition()); const UnknownElementDefinition unknown{}; ExpectModelFailure(factory.Create(unknown, beam_domain)); auto property_mismatch_definition = BeamDefinition(); property_mismatch_definition.shell_sections = { {"ShellSection", 0.2, 0U, {property_mismatch_definition.source_path, 21U}}}; property_mismatch_definition.elements[0U].section_index = 1U; const fesa::Domain property_mismatch = MakeDomain(std::move(property_mismatch_definition)); ExpectModelFailure( factory.Create(property_mismatch.Elements()[0U], property_mismatch)); const fesa::Domain material_mismatch = MakeDomain(ShellDefinition(0U, 1U)); ExpectModelFailure( factory.Create(material_mismatch.Elements()[0U], material_mismatch)); const fesa::Domain invalid_node = MakeDomain(BeamDefinition({0U, 2U})); ExpectModelFailure(factory.Create(invalid_node.Elements()[0U], invalid_node)); auto missing_frame_definition = ShellDefinition(); missing_frame_definition.shell_node_initial_frames.pop_back(); const fesa::Domain missing_frame = MakeDomain(std::move(missing_frame_definition)); auto missing_frame_result = factory.Create(missing_frame.Elements()[0U], missing_frame); ExpectModelFailure(missing_frame_result); EXPECT_EQ(missing_frame_result.GetStatus().Diagnostics()[0U].code, "missing-shell-frame"); }