#include "fesa/io/abaqus/domain_mapper.h" #include #include #include #include #include #include #include #include #include #include #include #include "fesa/elements/element_definition.h" #include "fesa/io/abaqus/input_reader.h" #include "fesa/math/vector3.h" namespace { class TemporaryInputFile { public: TemporaryInputFile(const std::string& stem, const std::string& content) : path_{std::filesystem::temp_directory_path() / ("fesa-domain-mapper-" + stem + ".inp")} { std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; stream.write(content.data(), static_cast(content.size())); if (!stream) { throw std::runtime_error{"Unable to create domain mapper 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 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::Failure(parsed.GetStatus()); } return fesa::AbaqusDomainMapper{}.Map(parsed.Value()); } std::string ReadExactBytes(const std::filesystem::path& path) { std::ifstream stream{path, std::ios::binary}; if (!stream) { throw std::runtime_error{"Unable to read legacy mapper fixture."}; } return std::string{std::istreambuf_iterator{stream}, std::istreambuf_iterator{}}; } std::filesystem::path RepositoryRoot() { auto path = std::filesystem::path{__FILE__}.parent_path(); for (int parent = 0; parent < 4; ++parent) { path = path.parent_path(); } return path; } const fesa::Diagnostic* FindDiagnostic(const fesa::Status& status, const std::string& code) { const auto found = std::find_if(status.Diagnostics().begin(), status.Diagnostics().end(), [&code](const fesa::Diagnostic& diagnostic) { return diagnostic.code == code; }); return found == status.Diagnostics().end() ? nullptr : &*found; } std::string ReplaceOnce(std::string text, const std::string& from, const std::string& to) { const auto position = text.find(from); if (position == std::string::npos) { throw std::logic_error{"Mapper test mutation source was not found."}; } text.replace(position, from.size(), to); return text; } std::string MinimalDeck() { return R"inp(*Part, name=BeamPart *Node 1, 0., 0., 0. 2, 1., 0., 0. *Element, type=B33 1, 1, 2 *Elset, elset=BeamSet 1 *Beam General Section, elset=BeamSet, material=Steel, section=GENERAL 1., 1., 0., 1., 1. 0., 1., 0. *End Part *Assembly, name=Assembly *Instance, name=Beam-1, part=BeamPart *End Instance *Nset, nset=Root, instance=Beam-1 1 *Nset, nset=Tip, instance=Beam-1 2 *End Assembly *Material, name=Steel *Elastic 100., 0.25 *Boundary Root, 1, 6 *Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. *Cload Tip, 2, -1. *End Step )inp"; } std::string ShellDeck() { return R"inp(*Part, name=ShellPart *Node 1, 0., 0., 0. 2, 1., 0., 0. 3, 1., 1., 0. 4, 0., 1., 0. 5, 2., 0., 0. 6, 2., 1., 0. *Element, type=S4 0010, 1, 2, 3, 4 *Element, type=S4R 0020, 2, 5, 6, 3 *Elset, elset=ShellS4 10 *Elset, elset=ShellS4R 20 *Shell Section, elset=ShellS4, material=Steel 0.1, 5 *Shell Section, elset=ShellS4R, material=Aluminum 0.2 *End Part *Assembly, name=Assembly *Instance, name=First, part=ShellPart *End Instance *Instance, name=Second, part=ShellPart *End Instance *Nset, nset=RootFirst, instance=First 1 *Nset, nset=TipSecond, instance=Second 6 *End Assembly *Material, name=Steel *Elastic 210000., 0.3 *Material, name=Aluminum *Elastic 70000., 0.25 *Boundary RootFirst, 1, 6 *Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. *Cload TipSecond, 6, 1. *End Step )inp"; } std::string SupportedInventoryDeck(bool include_no_ops) { const std::string preprint = include_no_ops ? "*Preprint, echo=NO, model=NO, history=NO, contact=NO\n" : ""; const std::string shear = include_no_ops ? "*Transverse Shear Stiffness\n1., 2., 3.\n" : ""; const std::string outputs = include_no_ops ? R"inp(*Restart, write, frequency=0 *Output, field *Node Output U, RF *Element Output, directions=YES S, SF *Contact Output, variable=PRESELECT *Output, history, variable=PRESELECT )inp" : ""; return std::string{R"inp(*hEaDiNg Every supported keyword )inp"} + preprint + R"inp(*pArT, NaMe=BeamPart *nOdE 0001, 0., 0., 0. 0002, 2., 0., 0. *eLeMeNt, TyPe=b33 0007, 0001, 0002 *nSeT, NsEt=RootLocal 0001 *Nset, nset=AllLocal, generate 1, 2, 1 *Elset, elset=BeamExplicit 0007 *eLsEt, ElSeT=BeamAll, GeNeRaTe 7, 7, 1 *Beam General Section, ELSET=beamall, MATERIAL=steel, SECTION=general 2., 3., 0., 4., 5. 0., 1., 0. *Section Points -0.5, 0.25 0.5, -0.25 )inp" + shear + R"inp(*eNd PaRt *aSsEmBlY, name=Assembly *iNsTaNcE, NAME=Beam-1, PART=beampart *eNd InStAnCe *Nset, nset=RootAssembly, instance=beam-1 0001 *Elset, elset=BeamAssembly, instance=BEAM-1 0007 *eNd AsSeMbLy *mAtErIaL, NaMe=Steel *eLaStIc 210000., 0.75 *bOuNdArY rootassembly, 1, 1, 0.125 *sTeP, name=User-Spelling, nLgEoM=no *sTaTiC 0.25, 1.5, 0.01, 1.5 *Boundary RootAssembly, 2, 2 *cLoAd RootAssembly, 6, -12.5 )inp" + outputs + "*eNd StEp\n"; } } // namespace // C-DUP-002 // C-MODEL-002 TEST(InpDomainMapping, MapsEverySupportedKeywordAndLegacyDeck) { auto result = MapText("supported-inventory", SupportedInventoryDeck(true)); ASSERT_TRUE(result.HasValue()); const fesa::Domain& domain = result.Value(); ASSERT_EQ(domain.Nodes().size(), 2U); EXPECT_EQ(domain.Nodes()[0].source_id.instance_name, "Beam-1"); EXPECT_EQ(domain.Nodes()[0].source_id.source_label, 1); EXPECT_EQ(domain.Nodes()[0].source_id.source_label_text, "0001"); EXPECT_EQ(domain.Nodes()[1].source_id.source_label_text, "0002"); EXPECT_DOUBLE_EQ(domain.Nodes()[1].coordinates[0], 2.0); ASSERT_EQ(domain.Elements().Size(), 1U); const fesa::ElementDefinition& element = domain.Elements()[0U]; EXPECT_EQ(element.Kind(), fesa::ElementDefinitionKind::kEulerBeam3D); EXPECT_EQ(element.SourceId().source_label_text, "0007"); EXPECT_EQ(element.SourceElementType(), "B33"); EXPECT_EQ(element.NodeIndices(), (std::vector{0U, 1U})); EXPECT_EQ(&element, &domain.BeamElements()[0U]); ASSERT_EQ(domain.Materials().Size(), 1U); EXPECT_EQ(domain.Materials()[0U].Kind(), fesa::MaterialKind::kIsotropicLinearElastic); EXPECT_EQ(domain.LinearElasticMaterials()[0U].name, "Steel"); EXPECT_DOUBLE_EQ(domain.LinearElasticMaterials()[0U].youngs_modulus, 210000.0); EXPECT_DOUBLE_EQ(domain.LinearElasticMaterials()[0U].poisson_ratio, 0.75); ASSERT_EQ(domain.Sections().Size(), 1U); const auto& section = domain.Sections()[0]; EXPECT_DOUBLE_EQ(section.area, 2.0); EXPECT_DOUBLE_EQ(section.i11, 3.0); EXPECT_DOUBLE_EQ(section.i12, 0.0); EXPECT_DOUBLE_EQ(section.i22, 4.0); EXPECT_DOUBLE_EQ(section.torsional_constant, 5.0); EXPECT_EQ(section.first_axis, (std::array{0.0, 1.0, 0.0})); EXPECT_EQ(section.section_points, (std::vector>{{-0.5, 0.25}, {0.5, -0.25}})); EXPECT_EQ(domain.BeamElements()[0U].material_index, 0U); EXPECT_EQ(domain.BeamElements()[0U].section_index, 0U); ASSERT_EQ(domain.Steps().Size(), 1U); const auto& step = domain.Steps()[0]; EXPECT_EQ(step.Name(), "Step-1"); EXPECT_DOUBLE_EQ(step.InitialIncrement(), 0.25); EXPECT_DOUBLE_EQ(step.TimePeriod(), 1.5); EXPECT_DOUBLE_EQ(step.MinimumIncrement(), 0.01); EXPECT_DOUBLE_EQ(step.MaximumIncrement(), 1.5); ASSERT_EQ(step.Boundaries().size(), 2U); EXPECT_EQ(step.Boundaries()[0].target, "rootassembly"); EXPECT_EQ(step.Boundaries()[0].first_dof, 1); EXPECT_DOUBLE_EQ(step.Boundaries()[0].value, 0.125); EXPECT_EQ(step.Boundaries()[1].last_dof, 2); EXPECT_DOUBLE_EQ(step.Boundaries()[1].value, 0.0); ASSERT_EQ(step.ConcentratedLoads().Size(), 1U); EXPECT_EQ(step.ConcentratedLoads()[0U].Target().target_name_or_label, "RootAssembly"); EXPECT_DOUBLE_EQ(step.ConcentratedLoads()[0U].GlobalComponents()[5U], -12.5); EXPECT_EQ(domain.Warnings().size(), 8U); const auto legacy_path = RepositoryRoot() / "reference" / "cantilever beam" / "cantilever beam.inp"; const auto bytes_before = ReadExactBytes(legacy_path); const auto timestamp_before = std::filesystem::last_write_time(legacy_path); auto parsed_legacy = fesa::AbaqusInputReader{}.Read(legacy_path); ASSERT_TRUE(parsed_legacy.HasValue()); auto legacy = fesa::AbaqusDomainMapper{}.Map(parsed_legacy.Value()); ASSERT_TRUE(legacy.HasValue()); EXPECT_EQ(legacy.Value().Nodes().size(), 11U); EXPECT_EQ(legacy.Value().Elements().Size(), 10U); EXPECT_EQ(legacy.Value().Materials().Size(), 1U); EXPECT_EQ(legacy.Value().Sections().Size(), 1U); EXPECT_EQ(legacy.Value().Steps().Size(), 1U); EXPECT_EQ(legacy.Value().Warnings().size(), 7U); EXPECT_EQ(ReadExactBytes(legacy_path), bytes_before); EXPECT_EQ(std::filesystem::last_write_time(legacy_path), timestamp_before); } TEST(InpDomainMapping, ExpandsSetsAndMultipleIdentityInstancesDeterministically) { const std::string deck = R"inp(*Part, name=P *Node 10, 0., 0., 0. 20, 1., 0., 0. *Element, type=B33 30, 10, 20 *Nset, nset=Ends, generate 10, 20, 10 *Elset, elset=AllElements 30 *Beam General Section, elset=AllElements, material=M, section=GENERAL 1., 2., 0., 3., 4. 0., 1., 0. *End Part *Assembly, name=A *Instance, name=First, part=P *End Instance *Instance, name=Second, part=P *End Instance *Nset, nset=OnlySecond, instance=Second 20 *Elset, elset=OnlySecondBeam, instance=Second 30 *End Assembly *Material, name=M *Elastic 1000., 0.25 *Step *Static 1., 1., 1., 1. *Boundary OnlySecond, 1, 1 *Cload OnlySecond, 2, 5. *End Step )inp"; auto result = MapText("multiple-instances", deck); ASSERT_TRUE(result.HasValue()); const fesa::Domain& domain = result.Value(); ASSERT_EQ(domain.Nodes().size(), 4U); EXPECT_EQ(domain.Nodes()[0].source_id.instance_name, "First"); EXPECT_EQ(domain.Nodes()[0].source_id.source_label, 10); EXPECT_EQ(domain.Nodes()[1].source_id.instance_name, "First"); EXPECT_EQ(domain.Nodes()[1].source_id.source_label, 20); EXPECT_EQ(domain.Nodes()[2].source_id.instance_name, "Second"); EXPECT_EQ(domain.Nodes()[2].source_id.source_label, 10); EXPECT_EQ(domain.Nodes()[3].source_id.instance_name, "Second"); EXPECT_EQ(domain.Nodes()[3].source_id.source_label, 20); ASSERT_EQ(domain.BeamElements().Size(), 2U); EXPECT_EQ(domain.BeamElements()[0].source_id.instance_name, "First"); EXPECT_EQ(domain.BeamElements()[0].node_indices, (std::array{0U, 1U})); EXPECT_EQ(domain.BeamElements()[1].source_id.instance_name, "Second"); EXPECT_EQ(domain.BeamElements()[1].node_indices, (std::array{2U, 3U})); ASSERT_EQ(domain.NodeSets().size(), 3U); EXPECT_EQ(domain.NodeSets()[0].name, "Ends"); EXPECT_EQ(domain.NodeSets()[0].instance_name, std::optional{"First"}); EXPECT_EQ(domain.NodeSets()[0].node_indices, (std::vector{0U, 1U})); EXPECT_EQ(domain.NodeSets()[1].instance_name, std::optional{"Second"}); EXPECT_EQ(domain.NodeSets()[1].node_indices, (std::vector{2U, 3U})); EXPECT_EQ(domain.NodeSets()[2].name, "OnlySecond"); EXPECT_EQ(domain.NodeSets()[2].node_indices, (std::vector{3U})); ASSERT_EQ(domain.ElementSets().size(), 3U); EXPECT_EQ(domain.ElementSets()[0].instance_name, std::optional{"First"}); EXPECT_EQ(domain.ElementSets()[0].element_indices, (std::vector{0U})); EXPECT_EQ(domain.ElementSets()[1].instance_name, std::optional{"Second"}); EXPECT_EQ(domain.ElementSets()[1].element_indices, (std::vector{1U})); EXPECT_EQ(domain.ElementSets()[2].name, "OnlySecondBeam"); EXPECT_EQ(domain.ElementSets()[2].element_indices, (std::vector{1U})); ASSERT_EQ(domain.Steps().Size(), 1U); EXPECT_EQ(domain.Steps()[0].Boundaries()[0].target, "OnlySecond"); EXPECT_EQ( domain.Steps()[0].ConcentratedLoads()[0U].Target().target_name_or_label, "OnlySecond"); auto direct = MapText("direct-node-labels", ReplaceOnce(ReplaceOnce(MinimalDeck(), "Root, 1, 6", "1, 1, 6"), "Tip, 2, -1.", "2, 2, -1.")); ASSERT_TRUE(direct.HasValue()); EXPECT_EQ(direct.Value().Steps()[0].Boundaries()[0].target, "1"); EXPECT_EQ(direct.Value() .Steps()[0] .ConcentratedLoads()[0U] .Target() .target_name_or_label, "2"); auto above_thresholds = MapText( "above-geometry-thresholds", ReplaceOnce( ReplaceOnce(MinimalDeck(), "2, 1., 0., 0.", "2, 2e-12, 0., 0."), "0., 1., 0.", "1., 2e-12, 0.")); ASSERT_TRUE(above_thresholds.HasValue()); auto large_finite = MapText( "large-finite-geometry", ReplaceOnce(ReplaceOnce(ReplaceOnce(MinimalDeck(), "1, 0., 0., 0.", "1, 1e308, 0., 0."), "2, 1., 0., 0.", "2, 1e308, 1e297, 0."), "0., 1., 0.", "1e308, 0., 0.")); ASSERT_TRUE(large_finite.HasValue()); const auto& large_domain = large_finite.Value(); ASSERT_EQ(large_domain.Nodes().size(), 2U); const fesa::Vector3 large_delta = fesa::Vector3{large_domain.Nodes()[1U].coordinates} - fesa::Vector3{large_domain.Nodes()[0U].coordinates}; EXPECT_TRUE(large_delta.IsFinite()); EXPECT_TRUE(std::isfinite(large_delta.Norm())); EXPECT_DOUBLE_EQ(large_delta.Y(), 1.0e297); const fesa::Vector3 large_guide{large_domain.Sections()[0U].first_axis}; EXPECT_TRUE(large_guide.IsFinite()); EXPECT_TRUE(std::isfinite(large_guide.Norm())); } TEST(InpDomainMapping, KeepsNodeAndElementSetNamesInSeparateNamespaces) { auto shared_name = MapText( "separate-part-set-namespaces", ReplaceOnce( ShellDeck(), "*Elset, elset=ShellS4\n10", "*Nset, nset=ShellS4\n1, 2, 3, 4\n*Elset, elset=ShellS4\n10")); ASSERT_TRUE(shared_name.HasValue()); const auto& domain = shared_name.Value(); EXPECT_TRUE(std::any_of( domain.NodeSets().begin(), domain.NodeSets().end(), [](const fesa::NodeSet& set) { return set.name == "ShellS4"; })); EXPECT_TRUE(std::any_of( domain.ElementSets().begin(), domain.ElementSets().end(), [](const fesa::ElementSet& set) { return set.name == "ShellS4"; })); auto duplicate_node_set = MapText("duplicate-part-node-set", ReplaceOnce(ShellDeck(), "*Elset, elset=ShellS4\n10", "*Nset, nset=Shared\n1\n*Nset, nset=Shared\n2\n" "*Elset, elset=ShellS4\n10")); ASSERT_FALSE(duplicate_node_set.HasValue()); EXPECT_NE(FindDiagnostic(duplicate_node_set.GetStatus(), "duplicate-entity"), nullptr); auto duplicate_element_set = MapText( "duplicate-part-element-set", ReplaceOnce(ShellDeck(), "*Elset, elset=ShellS4\n10", "*Elset, elset=Repeated\n10\n*Elset, elset=Repeated\n20\n" "*Elset, elset=ShellS4\n10")); ASSERT_FALSE(duplicate_element_set.HasValue()); EXPECT_NE( FindDiagnostic(duplicate_element_set.GetStatus(), "duplicate-entity"), nullptr); auto assembly_shared_name = MapText( "separate-assembly-set-namespaces", ReplaceOnce( ReplaceOnce(MinimalDeck(), "*Elset, elset=BeamSet\n1", "*Nset, nset=Root\n1, 2\n*Elset, elset=BeamSet\n1"), "*Nset, nset=Root, instance=Beam-1\n1", "*Nset, nset=Root, instance=Beam-1\n1\n" "*Elset, elset=Root, instance=Beam-1\n1")); ASSERT_TRUE(assembly_shared_name.HasValue()); const auto& assembly_domain = assembly_shared_name.Value(); const auto root_node_set_count = std::count_if( assembly_domain.NodeSets().begin(), assembly_domain.NodeSets().end(), [](const fesa::NodeSet& set) { return set.name == "Root"; }); EXPECT_EQ(root_node_set_count, 1); const auto root_node_set = std::find_if( assembly_domain.NodeSets().begin(), assembly_domain.NodeSets().end(), [](const fesa::NodeSet& set) { return set.name == "Root"; }); ASSERT_NE(root_node_set, assembly_domain.NodeSets().end()); EXPECT_EQ(root_node_set->node_indices, (std::vector{0U})); EXPECT_EQ(std::count_if( assembly_domain.ElementSets().begin(), assembly_domain.ElementSets().end(), [](const fesa::ElementSet& set) { return set.name == "Root"; }), 1); } TEST(InpDomainMapping, NoOpAllowlistWarnsWithoutSemanticEffect) { auto plain = MapText("without-no-ops", SupportedInventoryDeck(false)); auto with_no_ops = MapText("with-no-ops", SupportedInventoryDeck(true)); ASSERT_TRUE(plain.HasValue()); ASSERT_TRUE(with_no_ops.HasValue()); EXPECT_TRUE(plain.Value().Warnings().empty()); ASSERT_EQ(with_no_ops.Value().Warnings().size(), 8U); EXPECT_EQ(with_no_ops.Value().Warnings()[0].code, "ignored-input-keyword"); EXPECT_EQ(with_no_ops.Value().Warnings()[0].keyword, "PREPRINT"); EXPECT_EQ(with_no_ops.Value().Warnings()[1].keyword, "TRANSVERSE SHEAR STIFFNESS"); EXPECT_EQ(with_no_ops.Value().Warnings()[2].keyword, "RESTART"); EXPECT_EQ(with_no_ops.Value().Warnings()[3].keyword, "OUTPUT"); EXPECT_EQ(with_no_ops.Value().Warnings()[4].keyword, "NODE OUTPUT"); EXPECT_EQ(with_no_ops.Value().Warnings()[5].keyword, "ELEMENT OUTPUT"); EXPECT_EQ(with_no_ops.Value().Warnings()[6].keyword, "CONTACT OUTPUT"); EXPECT_EQ(with_no_ops.Value().Warnings()[7].keyword, "OUTPUT"); for (const auto& warning : with_no_ops.Value().Warnings()) { EXPECT_EQ(warning.severity, fesa::Severity::kWarning); } EXPECT_EQ(with_no_ops.Value().Nodes().size(), plain.Value().Nodes().size()); EXPECT_EQ(with_no_ops.Value().Elements().Size(), plain.Value().Elements().Size()); EXPECT_EQ(with_no_ops.Value().Materials().Size(), plain.Value().Materials().Size()); EXPECT_EQ(with_no_ops.Value().Sections().Size(), plain.Value().Sections().Size()); EXPECT_EQ(with_no_ops.Value().NodeSets().size(), plain.Value().NodeSets().size()); EXPECT_EQ(with_no_ops.Value().ElementSets().size(), plain.Value().ElementSets().size()); EXPECT_EQ(with_no_ops.Value().Steps().Size(), plain.Value().Steps().Size()); EXPECT_EQ(with_no_ops.Value().Steps()[0].Boundaries().size(), plain.Value().Steps()[0].Boundaries().size()); EXPECT_EQ(with_no_ops.Value().Steps()[0].Loads().size(), plain.Value().Steps()[0].Loads().size()); } // MITC4-MAP-001 // C-MODEL-002 TEST(InpDomainMapping, MapsS4AndS4rThroughOneMitc4Identity) { auto result = MapText("mitc4-map-001", ShellDeck()); ASSERT_TRUE(result.HasValue()); const fesa::Domain& domain = result.Value(); EXPECT_TRUE(domain.BeamElements().Empty()); ASSERT_EQ(domain.Elements().Size(), 4U); EXPECT_EQ(domain.Elements()[0U].Kind(), fesa::ElementDefinitionKind::kMitc4Shell); EXPECT_EQ(domain.Elements()[0U].SourceElementType(), "S4"); EXPECT_EQ(domain.Elements()[1U].SourceElementType(), "S4R"); EXPECT_EQ(&domain.Elements()[0U], &domain.ShellElements()[0U]); ASSERT_EQ(domain.ShellElements().Size(), 4U); EXPECT_EQ(domain.ShellElements()[0].source_id.instance_name, "First"); EXPECT_EQ(domain.ShellElements()[0].source_id.source_label_text, "0010"); EXPECT_EQ(domain.ShellElements()[0].source_type, fesa::ShellSourceElementType::kS4); EXPECT_EQ(domain.ShellElements()[0].node_indices, (std::array{0U, 1U, 2U, 3U})); EXPECT_EQ(domain.ShellElements()[0].section_index, 0U); EXPECT_EQ(domain.ShellElements()[0].material_index, 0U); EXPECT_EQ(domain.ShellElements()[1].source_id.instance_name, "First"); EXPECT_EQ(domain.ShellElements()[1].source_type, fesa::ShellSourceElementType::kS4r); EXPECT_EQ(domain.ShellElements()[1].node_indices, (std::array{1U, 4U, 5U, 2U})); EXPECT_EQ(domain.ShellElements()[1].section_index, 1U); EXPECT_EQ(domain.ShellElements()[1].material_index, 1U); EXPECT_EQ(domain.ShellElements()[2].source_id.instance_name, "Second"); EXPECT_EQ(domain.ShellElements()[2].node_indices, (std::array{6U, 7U, 8U, 9U})); EXPECT_EQ(domain.ShellElements()[3].source_id.instance_name, "Second"); EXPECT_EQ(fesa::kMitc4InternalFormulation, std::string_view{"FESA-MITC4"}); ASSERT_EQ(domain.ShellSections().Size(), 2U); EXPECT_EQ(domain.ShellSections()[0].name, "ShellS4"); EXPECT_DOUBLE_EQ(domain.ShellSections()[0].thickness, 0.1); EXPECT_EQ(domain.ShellSections()[0].material_index, 0U); EXPECT_EQ(domain.ShellSections()[1].name, "ShellS4R"); EXPECT_DOUBLE_EQ(domain.ShellSections()[1].thickness, 0.2); EXPECT_EQ(domain.ShellSections()[1].material_index, 1U); ASSERT_EQ(domain.ShellNodeInitialFrames().size(), domain.Nodes().size()); EXPECT_EQ(domain.ShellNodeInitialFrames()[0].node_index, 0U); EXPECT_EQ(domain.ShellNodeInitialFrames()[0].director, (std::array{0.0, 0.0, 1.0})); EXPECT_EQ(domain.ShellNodeInitialFrames()[0].tangent_a, (std::array{1.0, 0.0, 0.0})); EXPECT_EQ(domain.ShellNodeInitialFrames()[0].tangent_b, (std::array{0.0, 1.0, 0.0})); ASSERT_EQ(domain.Steps().Size(), 1U); ASSERT_EQ(domain.Steps()[0].Boundaries().size(), 1U); EXPECT_EQ(domain.Steps()[0].Boundaries()[0].last_dof, 6); ASSERT_EQ(domain.Steps()[0].ConcentratedLoads().Size(), 1U); EXPECT_DOUBLE_EQ( domain.Steps()[0].ConcentratedLoads()[0U].GlobalComponents()[5U], 1.0); } // MITC4-MAP-002 TEST(InpDomainMapping, RejectsInvalidShellAssignmentsAndProperties) { struct InvalidCase { std::string name; std::string deck; std::string expected_code; fesa::FailureCategory category; }; const std::string base = ShellDeck(); const std::vector cases{ {"unresolved-material", ReplaceOnce(base, "material=Steel", "material=Missing"), "unresolved-shell-section", fesa::FailureCategory::kInput}, {"unresolved-elset", ReplaceOnce(base, "elset=ShellS4, material=Steel", "elset=Missing, material=Steel"), "unresolved-shell-section", fesa::FailureCategory::kInput}, {"missing-assignment", ReplaceOnce(base, "*Shell Section, elset=ShellS4R, material=Aluminum\n0.2\n", ""), "invalid-shell-section-assignment", fesa::FailureCategory::kInput}, {"conflicting-assignment", ReplaceOnce(base, "elset=ShellS4R, material=Aluminum", "elset=ShellS4, material=Aluminum"), "invalid-shell-section-assignment", fesa::FailureCategory::kInput}, {"invalid-thickness", ReplaceOnce(base, "0.2\n*End Part", "0.\n*End Part"), "invalid-shell-thickness", fesa::FailureCategory::kModel}, {"invalid-material", ReplaceOnce(base, "70000., 0.25", "70000., 0.5"), "invalid-shell-material", fesa::FailureCategory::kModel}}; for (const auto& test_case : cases) { SCOPED_TRACE(test_case.name); auto result = MapText("mitc4-map-002-" + test_case.name, test_case.deck); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Category(), test_case.category); ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code), nullptr); } } // MITC4-MAP-003 TEST(InpDomainMapping, RejectsInvalidShellConnectivityOptionsAndMixedModels) { struct InvalidCase { std::string name; std::string deck; std::string expected_code; }; const std::string base = ShellDeck(); const std::vector cases{ {"wrong-arity", ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 3"), "invalid-shell-connectivity"}, {"repeated-node", ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 2, 4"), "invalid-shell-connectivity"}, {"dangling-node", ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 3, 99"), "invalid-shell-connectivity"}, {"unsupported-section-option", ReplaceOnce(base, "material=Steel\n0.1", "material=Steel, offset=0.1\n0.1"), "unsupported-shell-section-option"}, {"unsupported-element", ReplaceOnce(base, "type=S4R", "type=S8R"), "unsupported-element-formulation"}, {"mixed-beam-shell", ReplaceOnce(base, "0020, 2, 5, 6, 3\n*Elset", "0020, 2, 5, 6, 3\n*Element, type=B33\n30, 1, 2\n*Elset"), "unsupported-mixed-element-model"}}; for (const auto& test_case : cases) { SCOPED_TRACE(test_case.name); auto result = MapText("mitc4-map-003-" + test_case.name, test_case.deck); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code), nullptr); } } // MITC4-MAP-004 TEST(InpDomainMapping, PreservesProcedureLoadAndOutputRequestBoundariesForShells) { const std::string with_no_ops = ReplaceOnce( ShellDeck(), "*End Step\n", "*Output, field\n*Node Output\nU, RF\n*Element Output\nS\n*End Step\n"); auto valid = MapText("mitc4-map-004-no-ops", with_no_ops); ASSERT_TRUE(valid.HasValue()); ASSERT_EQ(valid.Value().Warnings().size(), 3U); EXPECT_EQ(valid.Value().Warnings()[0].keyword, "OUTPUT"); EXPECT_EQ(valid.Value().Warnings()[1].keyword, "NODE OUTPUT"); EXPECT_EQ(valid.Value().Warnings()[2].keyword, "ELEMENT OUTPUT"); struct InvalidCase { std::string name; std::string deck; std::string expected_code; }; const std::string base = ShellDeck(); const std::vector cases{ {"second-step", base + "*Step\n*Static\n1., 1., 1., 1.\n*End Step\n", "unsupported-multiple-step"}, {"nonlinear-step", ReplaceOnce(base, "nlgeom=NO", "nlgeom=YES"), "unsupported-nonlinear-geometry"}, {"other-procedure", ReplaceOnce(base, "*Static\n0.1, 1., 0.01, 1.", "*Dynamic\n0.1, 1., 0.01, 1."), "unsupported-keyword"}, {"distributed-load", ReplaceOnce(base, "*Cload\nTipSecond, 6, 1.", "*Dload\nShellS4, P, 1."), "unsupported-distributed-load"}}; for (const auto& test_case : cases) { SCOPED_TRACE(test_case.name); auto result = MapText("mitc4-map-004-" + test_case.name, test_case.deck); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code), nullptr); } } TEST(InpDomainMapping, RejectsUnsupportedAndInvalidPortfolio) { struct InvalidCase { std::string name; std::string deck; std::string expected_code; fesa::FailureCategory category; }; const std::string base = MinimalDeck(); const std::vector cases{ {"b31", ReplaceOnce(base, "type=B33", "type=B31"), "unsupported-element-formulation", fesa::FailureCategory::kInput}, {"transform", ReplaceOnce(base, "*End Instance\n", "1., 0., 0.\n*End Instance\n"), "unsupported-instance-transform", fesa::FailureCategory::kInput}, {"nested-assembly", ReplaceOnce(base, "*End Assembly\n", "*Assembly, name=Nested\n*End Assembly\n*End Assembly\n"), "unsupported-nested-assembly", fesa::FailureCategory::kInput}, {"multiple-step", base + "*Step\n*Static\n1., 1., 1., 1.\n*End Step\n", "unsupported-multiple-step", fesa::FailureCategory::kInput}, {"late-part", ReplaceOnce(base, "*End Assembly\n*Material", "*End Assembly\n*Part, name=Late\n*End Part\n*Material"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"material-before-assembly", ReplaceOnce(base, "*Assembly, name=Assembly", "*Material, name=Early\n*Elastic\n50., 0.2\n*Assembly, " "name=Assembly"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"material-after-model-boundary", ReplaceOnce(base, "*Material, name=Steel\n*Elastic\n100., " "0.25\n*Boundary\nRoot, 1, 6", "*Boundary\nRoot, 1, 6\n*Material, " "name=Steel\n*Elastic\n100., 0.25"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"keyword-after-step", base + "*Preprint, echo=NO\n", "invalid-keyword-location", fesa::FailureCategory::kInput}, {"assembly-instance-after-set", ReplaceOnce(base, "*Instance, name=Beam-1, part=BeamPart\n*End " "Instance\n*Nset, nset=Root, instance=Beam-1\n1", "*Nset, nset=Root, instance=Beam-1\n1\n*Instance, " "name=Beam-1, part=BeamPart\n*End Instance"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"element-before-node", ReplaceOnce( base, "*Node\n1, 0., 0., 0.\n2, 1., 0., 0.\n*Element, type=B33\n1, 1, 2", "*Element, type=B33\n1, 1, 2\n*Node\n1, 0., 0., 0.\n2, 1., 0., " "0."), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"shear-before-section-context", ReplaceOnce( base, "*Beam General Section", "*Transverse Shear Stiffness\n1., 2., 3.\n*Beam General Section"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"incomplete-part", ReplaceOnce(base, base.substr(base.find("*Part"), base.find("*End Part") + std::string{"*End Part\n"}.size() - base.find("*Part")), "*Part, name=BeamPart\n*End Part\n"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"empty-assembly", ReplaceOnce(base, base.substr(base.find("*Assembly"), base.find("*End Assembly") + std::string{"*End Assembly\n"}.size() - base.find("*Assembly")), "*Assembly, name=Assembly\n*End Assembly\n"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"cload-before-static", ReplaceOnce(base, "*Static\n0.1, 1., 0.01, 1.\n*Cload\nTip, 2, -1.", "*Cload\nTip, 2, -1.\n*Static\n0.1, 1., 0.01, 1."), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"static-after-boundary", ReplaceOnce(ReplaceOnce(base, "*Boundary\nRoot, 1, 6\n*Step", "*Step"), "*Static\n0.1, 1., 0.01, 1.", "*Boundary\nRoot, 1, 6\n*Static\n0.1, 1., 0.01, 1."), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"boundary-after-cload", ReplaceOnce(ReplaceOnce(base, "*Boundary\nRoot, 1, 6\n*Step", "*Step"), "Tip, 2, -1.\n*End Step", "Tip, 2, -1.\n*Boundary\nRoot, 1, 6\n*End Step"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"cload-after-no-op", ReplaceOnce(base, "*Cload", "*Restart, write, frequency=0\n*Cload"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"step-without-static", ReplaceOnce(base, "*Static\n0.1, 1., 0.01, 1.\n*Cload\nTip, 2, -1.\n*End Step", "*End Step"), "invalid-keyword-location", fesa::FailureCategory::kInput}, {"dependent-instance", ReplaceOnce(base, "part=BeamPart", "part=BeamPart, dependent=YES"), "unsupported-instance-mesh-semantics", fesa::FailureCategory::kInput}, {"coupled-section", ReplaceOnce(base, "1., 1., 0., 1., 1.", "1., 1., 0.5, 1., 1."), "unsupported-coupled-section", fesa::FailureCategory::kModel}, {"zero-length", ReplaceOnce(base, "2, 1., 0., 0.", "2, 0., 0., 0."), "invalid-beam-length", fesa::FailureCategory::kModel}, {"parallel-guide", ReplaceOnce(base, "0., 1., 0.", "1., 0., 0."), "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, {"nonpositive-area", ReplaceOnce(base, "1., 1., 0., 1., 1.", "0., 1., 0., 1., 1."), "invalid-beam-property", fesa::FailureCategory::kModel}, {"nonpositive-derived-shear", ReplaceOnce(base, "100., 0.25", "100., -1.25"), "invalid-beam-property", fesa::FailureCategory::kModel}, {"nonfinite-elastic", ReplaceOnce(base, "100., 0.25", "inf, 0.25"), "invalid-beam-property", fesa::FailureCategory::kModel}, {"nonfinite-section-property", ReplaceOnce(base, "1., 1., 0., 1., 1.", "nan, 1., 0., 1., 1."), "invalid-beam-property", fesa::FailureCategory::kModel}, {"nonfinite-guide", ReplaceOnce(base, "0., 1., 0.", "0., inf, 0."), "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, {"length-at-threshold", ReplaceOnce(base, "2, 1., 0., 0.", "2, 1e-12, 0., 0."), "invalid-beam-length", fesa::FailureCategory::kModel}, {"guide-at-threshold", ReplaceOnce(base, "0., 1., 0.", "1., 1e-12, 0."), "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, {"duplicate-elastic", ReplaceOnce(base, "100., 0.25\n*Boundary", "100., 0.25\n*Elastic\n100., 0.25\n*Boundary"), "duplicate-entity", fesa::FailureCategory::kInput}, {"duplicate-node-label", ReplaceOnce(base, "2, 1., 0., 0.", "1, 1., 0., 0."), "duplicate-entity", fesa::FailureCategory::kInput}, {"dangling-connectivity", ReplaceOnce(base, "1, 1, 2", "1, 1, 9"), "unresolved-reference", fesa::FailureCategory::kInput}, {"invalid-dof", ReplaceOnce(base, "Root, 1, 6", "Root, 1, 7"), "invalid-dof", fesa::FailureCategory::kInput}, {"nonfinite-coordinate", ReplaceOnce(base, "1., 0., 0.", "nan, 0., 0."), "invalid-numeric-value", fesa::FailureCategory::kInput}, {"malformed-node-arity", ReplaceOnce(base, "1, 0., 0., 0.", "1, 0., 0."), "invalid-data-arity", fesa::FailureCategory::kInput}, {"nlgeom", ReplaceOnce(base, "nlgeom=NO", "nlgeom=YES"), "unsupported-nonlinear-geometry", fesa::FailureCategory::kModel}, {"unknown-keyword", ReplaceOnce(base, "*Assembly, name=Assembly", "*Density\n1.\n*Assembly, name=Assembly"), "unsupported-keyword", fesa::FailureCategory::kInput}, {"invalid-static-arity", ReplaceOnce(base, "0.1, 1., 0.01, 1.", "0.1, 1., 0.01"), "invalid-static-data", fesa::FailureCategory::kInput}, {"invalid-static-range", ReplaceOnce(base, "0.1, 1., 0.01, 1.", "0.1, 1., 2., 1."), "invalid-static-data", fesa::FailureCategory::kInput}, {"invalid-generate", ReplaceOnce(base, "*Elset, elset=BeamSet\n1", "*Elset, elset=BeamSet, generate\n1, 1, 0"), "invalid-set-range", fesa::FailureCategory::kInput}, {"nonlanding-generate", ReplaceOnce(base, "*Elset, elset=BeamSet\n1", "*Elset, elset=BeamSet, generate\n1, 2, 2"), "invalid-set-range", fesa::FailureCategory::kInput}, {"ambiguous-direct-label", ReplaceOnce( ReplaceOnce(base, "*End Instance\n*Nset, nset=Root", "*End Instance\n*Instance, name=Beam-2, " "part=BeamPart\n*End Instance\n*Nset, nset=Root"), "Root, 1, 6", "1, 1, 6"), "unresolved-reference", fesa::FailureCategory::kInput}, {"ambiguous-part-set", ReplaceOnce( ReplaceOnce( ReplaceOnce(base, "*Elset, elset=BeamSet", "*Nset, nset=Local\n1\n*Elset, elset=BeamSet"), "*End Instance\n*Nset, nset=Root", "*End Instance\n*Instance, name=Beam-2, part=BeamPart\n*End " "Instance\n*Nset, nset=Root"), "Root, 1, 6", "Local, 1, 6"), "unresolved-reference", fesa::FailureCategory::kInput}, {"direct-set-conflict", ReplaceOnce(base, "*Step, name=Load", "*Boundary\n1, 1, 1, 2.\n*Step, name=Load"), "conflicting-boundary-condition", fesa::FailureCategory::kInput}, {"dangling-boundary-target", ReplaceOnce(base, "Root, 1, 6", "Missing, 1, 6"), "unresolved-reference", fesa::FailureCategory::kInput}, {"conflicting-boundary", ReplaceOnce(base, "*Step, name=Load", "*Boundary\nRoot, 1, 1, 2.\n*Step, name=Load"), "conflicting-boundary-condition", fesa::FailureCategory::kInput}}; for (const auto& test_case : cases) { SCOPED_TRACE(test_case.name); auto result = MapText("invalid-" + test_case.name, test_case.deck); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Category(), test_case.category); const auto* diagnostic = FindDiagnostic(result.GetStatus(), test_case.expected_code); ASSERT_NE(diagnostic, nullptr); EXPECT_EQ(diagnostic->severity, fesa::Severity::kError); EXPECT_FALSE(diagnostic->location.file.empty()); EXPECT_GT(diagnostic->location.line, 0U); } struct SameTokenAmbiguityCase { std::string name; std::string deck; std::string expected_keyword; std::size_t expected_line; }; const std::string same_token_base = ReplaceOnce( base, "*Elset, elset=BeamSet", "*Nset, nset=1\n2\n*Elset, elset=BeamSet"); const std::vector same_token_cases{ {"boundary", ReplaceOnce(same_token_base, "Root, 1, 6", "1, 1, 6"), "BOUNDARY", 27U}, {"cload", ReplaceOnce(same_token_base, "Tip, 2, -1.", "1, 2, -1."), "CLOAD", 32U}}; for (const auto& test_case : same_token_cases) { SCOPED_TRACE("same-token-" + test_case.name); auto result = MapText("same-token-" + test_case.name, test_case.deck); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); const auto* diagnostic = FindDiagnostic(result.GetStatus(), "unresolved-reference"); ASSERT_NE(diagnostic, nullptr); EXPECT_EQ(diagnostic->severity, fesa::Severity::kError); EXPECT_EQ(diagnostic->keyword, test_case.expected_keyword); EXPECT_EQ(diagnostic->entity_identity, "1"); EXPECT_EQ(diagnostic->location.line, test_case.expected_line); EXPECT_EQ(diagnostic->location.file.filename().string(), "fesa-domain-mapper-same-token-" + test_case.name + ".inp"); } } TEST(InpDomainMapping, RejectsDloadWithoutDistributedLoadObject) { const auto deck = ReplaceOnce(MinimalDeck(), "*Cload\nTip, 2, -1.\n", "*Dload\nBeamSet, PY, -1.\n"); auto result = MapText("dload", deck); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); const auto* diagnostic = FindDiagnostic(result.GetStatus(), "unsupported-keyword"); ASSERT_NE(diagnostic, nullptr); EXPECT_EQ(diagnostic->keyword, "DLOAD"); }