#include "fesa/analysis/analysis_model.h" #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" #include #include #include #include #include #include #include #include namespace { class TemporaryInputFile { public: TemporaryInputFile(const std::string& stem, const std::string& content) : path_{std::filesystem::temp_directory_path() / ("fesa-shell-surface-pressure-" + 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 pressure input 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}; const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); if (!parsed.HasValue()) { return fesa::Result::Failure(parsed.GetStatus()); } return fesa::AbaqusDomainMapper{}.Map(parsed.Value()); } std::string ShellDeck(const std::string& assembly_content, const std::string& dsload_rows) { return std::string{R"inp(*Part, name=Shell-Part *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=S4-Set 0010 *Elset, elset=S4R-Set 0020 *Shell Section, elset=S4-Set, material=Steel 0.1 *Shell Section, elset=S4R-Set, material=Steel 0.1 *End Part *Assembly, name=Assembly *Instance, name=Instance-B, part=Shell-Part *End Instance *Instance, name=Instance-A, part=Shell-Part *End Instance )inp"} + assembly_content + R"inp(*End Assembly *Material, name=Steel *Elastic 210000., 0.3 *Step, name=Pressure-Step, nlgeom=NO *Static 0.1, 1., 0.01, 1. *Dsload )inp" + dsload_rows + "*End Step\n"; } const fesa::ElementSurfaceDefinition& OnlySurface(const fesa::Domain& domain) { EXPECT_EQ(domain.ElementSurfaces().size(), 1U); return domain.ElementSurfaces().front(); } } // namespace TEST(ShellSurfacePressureInput, MapsDirectInstanceFacetsForS4AndS4rCaseInsensitively) { auto result = MapText( "direct", ShellDeck("*SuRfAcE, NAME=Direct-Surface, TYPE=ELEMENT\n" "instance-a.0010, spos\n" "INSTANCE-A.0020, sneg\n", "direct-surface, P, 1.0\n")); ASSERT_TRUE(result.HasValue()); const auto& surface = OnlySurface(result.Value()); EXPECT_EQ(surface.surface_name_raw, "Direct-Surface"); EXPECT_EQ(surface.surface_name_canonical, "DIRECT-SURFACE"); ASSERT_EQ(surface.facets.size(), 2U); EXPECT_EQ(surface.facets[0].instance_name_raw, "Instance-A"); EXPECT_EQ(surface.facets[0].source_element_label, 10); EXPECT_EQ(surface.facets[0].source_element_label_raw, "0010"); EXPECT_EQ(surface.facets[0].source_element_type, "S4"); EXPECT_EQ(surface.facets[0].side, fesa::ShellSurfaceSide::kSpos); EXPECT_EQ(surface.facets[1].source_element_type, "S4R"); EXPECT_EQ(surface.facets[1].side, fesa::ShellSurfaceSide::kSneg); } TEST(ShellSurfacePressureInput, MapsExplicitInstanceElsetAndResultNeutralInternalParameter) { auto result = MapText( "elset", ShellDeck("*ELSET, ELSET=Flat-Members, INTERNAL, INSTANCE=Instance-A\n" "10, 20\n" "*SURFACE, TYPE=ELEMENT, NAME=Elset-Surface\n" "flat-members, SPOS\n", "Elset-Surface, P, 5.0\n")); ASSERT_TRUE(result.HasValue()); const auto& surface = OnlySurface(result.Value()); ASSERT_EQ(surface.facets.size(), 2U); for (const auto& facet : surface.facets) { EXPECT_EQ(facet.member_kind, fesa::SurfaceMemberKind::kElset); EXPECT_EQ(facet.elset_name_raw, "flat-members"); EXPECT_EQ(facet.elset_name_canonical, "FLAT-MEMBERS"); EXPECT_EQ(facet.instance_name_raw, "Instance-A"); EXPECT_EQ(facet.side, fesa::ShellSurfaceSide::kSpos); } } TEST(ShellSurfacePressureInput, MapsMixedDirectAndElsetMembersAcrossTwoExplicitInstancesWithStableDataMemberAndElsetOrder) { auto result = MapText( "mixed", ShellDeck("*ELSET, ELSET=Flat-B, INSTANCE=instance-b\n" "20\n" "*ELSET, ELSET=Flat-A, INSTANCE=INSTANCE-A\n" "10\n" "*SURFACE, TYPE=ELEMENT, NAME=Mixed-Surface\n" "instance-a.20, SNEG\n" "flat-b, SPOS\n" "INSTANCE-B.10, sneg\n" "FLAT-A, spos\n", "Mixed-Surface, P, 3.0\n")); ASSERT_TRUE(result.HasValue()); const auto& surface = OnlySurface(result.Value()); ASSERT_EQ(surface.facets.size(), 4U); EXPECT_EQ(surface.facets[0].instance_name_raw, "Instance-B"); EXPECT_EQ(surface.facets[0].source_element_label, 10); EXPECT_EQ(surface.facets[0].side, fesa::ShellSurfaceSide::kSneg); EXPECT_EQ(surface.facets[0].first_member_ordinal, 2U); EXPECT_EQ(surface.facets[1].instance_name_raw, "Instance-B"); EXPECT_EQ(surface.facets[1].source_element_label, 20); EXPECT_EQ(surface.facets[1].member_kind, fesa::SurfaceMemberKind::kElset); EXPECT_EQ(surface.facets[1].first_member_ordinal, 1U); EXPECT_EQ(surface.facets[2].instance_name_raw, "Instance-A"); EXPECT_EQ(surface.facets[2].source_element_label, 10); EXPECT_EQ(surface.facets[2].member_kind, fesa::SurfaceMemberKind::kElset); EXPECT_EQ(surface.facets[2].first_member_ordinal, 3U); EXPECT_EQ(surface.facets[3].instance_name_raw, "Instance-A"); EXPECT_EQ(surface.facets[3].source_element_label, 20); EXPECT_EQ(surface.facets[3].member_kind, fesa::SurfaceMemberKind::kDirect); EXPECT_EQ(surface.facets[3].first_member_ordinal, 0U); for (const auto& facet : surface.facets) { EXPECT_EQ(facet.part_name_raw, "Shell-Part"); EXPECT_EQ(facet.part_name_canonical, "SHELL-PART"); EXPECT_EQ(facet.instance_name_canonical, facet.instance_name_raw == "Instance-A" ? "INSTANCE-A" : "INSTANCE-B"); } } TEST(ShellSurfacePressureInput, PreservesSposSnegMagnitudeTextLocationsAndSourceOrder) { auto result = MapText( "provenance", ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" "Instance-A.10, SPOS\n" "Instance-A.20, SNEG\n", "pressure-surface, p, -0.0\n" "PRESSURE-SURFACE, P, +2.50e+03\n")); ASSERT_TRUE(result.HasValue()); const auto& surface = OnlySurface(result.Value()); ASSERT_EQ(surface.facets.size(), 2U); EXPECT_EQ(surface.facets[0].facet_ordinal, 0U); EXPECT_EQ(surface.facets[0].side, fesa::ShellSurfaceSide::kSpos); EXPECT_EQ(surface.facets[1].facet_ordinal, 1U); EXPECT_EQ(surface.facets[1].side, fesa::ShellSurfaceSide::kSneg); const auto& step = result.Value().Steps()[0U]; ASSERT_EQ(step.SurfacePressureLoads().size(), 2U); const auto& first = step.SurfacePressureLoads()[0U]; const auto& second = step.SurfacePressureLoads()[1U]; EXPECT_EQ(first.step_name_raw, "Pressure-Step"); EXPECT_EQ(first.step_name_canonical, "PRESSURE-STEP"); EXPECT_EQ(first.surface_name_raw, "pressure-surface"); EXPECT_EQ(first.surface_name_canonical, "PRESSURE-SURFACE"); EXPECT_EQ(first.load_type, "P"); EXPECT_DOUBLE_EQ(first.magnitude, 0.0); EXPECT_EQ(first.magnitude_raw, "-0.0"); EXPECT_EQ(first.data_ordinal, 0U); EXPECT_EQ(first.load_ordinal, 0U); EXPECT_LT(first.keyword_location.line, first.data_location.line); EXPECT_EQ(second.magnitude_raw, "+2.50e+03"); EXPECT_DOUBLE_EQ(second.magnitude, 2500.0); EXPECT_EQ(second.data_ordinal, 1U); EXPECT_EQ(second.load_ordinal, 1U); } TEST(ShellSurfacePressureDomain, OwnsCanonicalFacetsAndStepOwnedAdditivePressureRecords) { fesa::ModelDefinition definition{}; definition.element_surfaces = { {"Surface", "SURFACE", {{0U, 0U, "Part", "PART", "Instance", "INSTANCE", 10, "0010", "S4", fesa::ShellSurfaceSide::kSpos, {"owned.inp", 3U}, 0U, fesa::SurfaceMemberKind::kDirect, "", ""}}, {"owned.inp", 2U}, 0U}}; definition.steps = {{"Step", {}, {}, {{"Step", "STEP", 0U, "Surface", "SURFACE", "P", 1.0, "1.0", {"owned.inp", 7U}, {"owned.inp", 8U}, 0U, 0U}}, fesa::StaticProcedureKind::kLinearSmallDisplacement, false, 0.1, 1.0, 0.01, 1.0, {"owned.inp", 6U}}}; auto result = fesa::Domain::Create(definition); ASSERT_TRUE(result.HasValue()); definition.element_surfaces[0].facets[0].source_element_label = 99; definition.steps[0].surface_pressure_loads[0].magnitude = 99.0; const fesa::Domain& domain = result.Value(); static_assert(std::is_same_v< decltype(std::declval().ElementSurfaces()), const std::vector&>); static_assert(std::is_same_v< decltype(std::declval() .SurfacePressureLoads()), const std::vector&>); EXPECT_EQ(domain.ElementSurfaces()[0].facets[0].source_element_label, 10); EXPECT_DOUBLE_EQ(domain.Steps()[0U].SurfacePressureLoads()[0].magnitude, 1.0); } TEST(ShellSurfacePressureDomain, AnalysisModelBorrowsActivePressureWithoutCopyingDomain) { auto mapped = MapText( "analysis-model", ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" "Instance-A.10, SPOS\n", "Pressure-Surface, P, 1.0\n")); ASSERT_TRUE(mapped.HasValue()); auto model = fesa::AnalysisModel::Create(mapped.Value()); ASSERT_TRUE(model.HasValue()); EXPECT_EQ(&model.Value().ActiveSurfacePressureLoads(), &mapped.Value().Steps()[0U].SurfacePressureLoads()); EXPECT_EQ(model.Value().ActiveSurfacePressureLoads().size(), 1U); } TEST(ShellSurfacePressureInput, AcceptsWhitespaceSignedZeroAndFiniteScientificNotation) { auto result = MapText( "numbers", ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" "Instance-A.10, SPOS\n", " pressure-surface , p , +0 \n" "Pressure-Surface, P, -1.25E-3\n")); ASSERT_TRUE(result.HasValue()); const auto& loads = result.Value().Steps()[0U].SurfacePressureLoads(); ASSERT_EQ(loads.size(), 2U); EXPECT_EQ(loads[0].magnitude_raw, "+0"); EXPECT_DOUBLE_EQ(loads[0].magnitude, 0.0); EXPECT_EQ(loads[1].magnitude_raw, "-1.25E-3"); EXPECT_DOUBLE_EQ(loads[1].magnitude, -0.00125); }