#include #include #include #include #include #include #include #include #include #include #include #include namespace { class TemporaryDeck final { public: TemporaryDeck(std::string_view name, std::string_view contents) : path_{std::filesystem::path{testing::TempDir()} / name} { std::ofstream output{path_, std::ios::binary}; output.write( contents.data(), static_cast(contents.size())); if (!output) { throw std::runtime_error{"Failed to write temporary Abaqus deck."}; } } ~TemporaryDeck() { std::error_code error; std::filesystem::remove(path_, error); } TemporaryDeck(const TemporaryDeck&) = delete; TemporaryDeck& operator=(const TemporaryDeck&) = delete; [[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; } private: std::filesystem::path path_; }; fesa::DomainBuildResult parse_and_map(const TemporaryDeck& input) { auto parsed = fesa::parse_deck(input.path()); if (!parsed.deck.has_value()) { return {std::nullopt, std::move(parsed.diagnostics)}; } return fesa::map_deck_to_domain(*parsed.deck); } void expect_diagnostic( const fesa::DomainBuildResult& result, const std::string_view code, const std::size_t line) { EXPECT_FALSE(result.domain.has_value()); const auto diagnostic = std::ranges::find_if( result.diagnostics, [code, line](const fesa::Diagnostic& candidate) { return candidate.stage == fesa::DiagnosticStage::semantic && candidate.code == code && candidate.source.has_value() && candidate.source->line == line; }); EXPECT_NE(diagnostic, result.diagnostics.end()); } std::string_view valid_flat_prefix() { return "*NODE\n" "1, 0.0, 0.0, 0.0\n" "2, 1.0, 0.0, 0.0\n" "*ELEMENT, TYPE=B31, ELSET=Beam\n" "1, 1, 2\n" "*ELSET, ELSET=Beam\n" "1\n" "*MATERIAL, NAME=Steel\n" "*ELASTIC\n" "200.0, 0.25\n"; } std::string complete_flat_deck(std::string_view section_and_shear) { std::string contents{valid_flat_prefix()}; contents.append(section_and_shear); contents.append( "*STEP\n" "*STATIC\n" "*END STEP\n"); return contents; } TEST(MaterialMapping, ResolvesForwardMaterialAndExplicitElsetAssignment) { const TemporaryDeck input{ "fesa-forward-material.inp", "*PART, NAME=BeamPart\n" "*NODE\n" "1, 0.0, 0.0, 0.0\n" "2, 1.0, 0.0, 0.0\n" "*ELEMENT, TYPE=B31\n" "1, 1, 2\n" "*ELSET, ELSET=Beam\n" "1\n" "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n" "*END PART\n" "*ASSEMBLY, NAME=Assembly\n" "*INSTANCE, NAME=Beam-1, PART=BeamPart\n" "*END INSTANCE\n" "*END ASSEMBLY\n" "*MATERIAL, NAME=Steel\n" "*ELASTIC\n" "200.0, 0.25\n" "*STEP\n" "*STATIC\n" "*END STEP\n"}; const auto result = parse_and_map(input); ASSERT_TRUE(result.domain.has_value()); ASSERT_TRUE(result.diagnostics.empty()); ASSERT_EQ(result.domain->materials().size(), 1U); EXPECT_DOUBLE_EQ(result.domain->materials()[0].young, 200.0); EXPECT_DOUBLE_EQ(result.domain->materials()[0].poisson, 0.25); ASSERT_EQ(result.domain->sections().size(), 1U); EXPECT_EQ( result.domain->beam_elements()[0].material, result.domain->materials()[0].id); EXPECT_EQ( result.domain->beam_elements()[0].section, result.domain->sections()[0].id); } TEST(ShearDefault, PreservesPhase1DefaultEffectiveAreasAndSource) { const TemporaryDeck input{ "fesa-default-shear.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n")}; const auto result = parse_and_map(input); ASSERT_TRUE(result.domain.has_value()); ASSERT_EQ(result.domain->sections().size(), 1U); const fesa::BeamSection& section = result.domain->sections()[0]; EXPECT_DOUBLE_EQ(section.shear_area_y, 1.0); EXPECT_DOUBLE_EQ(section.shear_area_z, 1.0); EXPECT_EQ( section.shear_source, fesa::ShearPropertySource::phase1_default); } TEST(ShearDefault, ConvertsExplicitK23AndK13UsingIsotropicShearModulus) { const TemporaryDeck input{ "fesa-explicit-shear.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n" "*TRANSVERSE SHEAR STIFFNESS\n" "64.0, 40.0, 0.0\n")}; const auto result = parse_and_map(input); ASSERT_TRUE(result.domain.has_value()); ASSERT_EQ(result.domain->sections().size(), 1U); const fesa::BeamSection& section = result.domain->sections()[0]; EXPECT_DOUBLE_EQ(section.shear_area_y, 0.8); EXPECT_DOUBLE_EQ(section.shear_area_z, 0.5); EXPECT_EQ(section.shear_source, fesa::ShearPropertySource::input); } TEST(BeamSection, RejectsInvalidElasticPropertiesAtTheDataRow) { const TemporaryDeck input{ "fesa-invalid-elastic.inp", "*MATERIAL, NAME=Steel\n" "*ELASTIC\n" "-1.0, 0.25\n" "*STEP\n" "*STATIC\n" "*END STEP\n"}; const auto result = parse_and_map(input); expect_diagnostic( result, "abaqus.semantic.invalid_elastic_data", 3U); } TEST(BeamSection, RejectsMaterialWithoutElasticData) { const TemporaryDeck input{ "fesa-missing-elastic.inp", "*MATERIAL, NAME=Steel\n" "*STEP\n" "*STATIC\n" "*END STEP\n"}; const auto result = parse_and_map(input); expect_diagnostic(result, "abaqus.semantic.missing_elastic", 1U); } TEST(MaterialMapping, RejectsDuplicateMaterialDefinition) { const TemporaryDeck input{ "fesa-duplicate-material.inp", "*MATERIAL, NAME=Steel\n" "*ELASTIC\n" "200.0, 0.25\n" "*MATERIAL, NAME=Steel\n" "*ELASTIC\n" "210.0, 0.3\n" "*STEP\n" "*STATIC\n" "*END STEP\n"}; const auto result = parse_and_map(input); expect_diagnostic( result, "abaqus.semantic.duplicate_material", 4U); } TEST(MaterialMapping, RejectsSectionWithMissingMaterialReference) { const TemporaryDeck input{ "fesa-missing-material.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Missing\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n")}; const auto result = parse_and_map(input); expect_diagnostic(result, "abaqus.semantic.missing_material", 11U); } TEST(BeamSection, RejectsNonzeroProductMomentAtTheDataRow) { const TemporaryDeck input{ "fesa-nonzero-iyz.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.2, 2.0, 0.1, 3.0, 4.0\n" "0.0, 1.0, 0.0\n")}; const auto result = parse_and_map(input); expect_diagnostic( result, "abaqus.semantic.invalid_section_data", 12U); } TEST(BeamSection, RejectsNonpositiveGeneralPropertyAtTheDataRow) { const TemporaryDeck input{ "fesa-invalid-area.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "0.0, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n")}; const auto result = parse_and_map(input); expect_diagnostic( result, "abaqus.semantic.invalid_section_data", 12U); } TEST(BeamSection, RejectsNonfiniteOrientationAtTheOrientationRow) { const TemporaryDeck input{ "fesa-nonfinite-orientation.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, nan, 0.0\n")}; const auto result = parse_and_map(input); expect_diagnostic( result, "abaqus.semantic.invalid_section_data", 13U); } TEST(BeamSection, RejectsSectionWhoseElementSetDoesNotExist) { const TemporaryDeck input{ "fesa-missing-section-elset.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Missing, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n")}; const auto result = parse_and_map(input); expect_diagnostic( result, "abaqus.semantic.missing_element_set", 11U); } TEST(BeamSection, RejectsNonzeroScfAtTheDataRow) { const TemporaryDeck input{ "fesa-nonzero-scf.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n" "*TRANSVERSE SHEAR STIFFNESS\n" "64.0, 40.0, 0.25\n")}; const auto result = parse_and_map(input); expect_diagnostic(result, "abaqus.semantic.nonzero_scf", 15U); } TEST(BeamSection, RejectsElementWithoutSectionAssignment) { const TemporaryDeck input{ "fesa-missing-section.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Other, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n" "*ELSET, ELSET=Other\n")}; const auto result = parse_and_map(input); expect_diagnostic(result, "abaqus.semantic.missing_section", 4U); } TEST(BeamSection, RejectsDuplicateSectionAssignment) { const TemporaryDeck input{ "fesa-duplicate-section.inp", complete_flat_deck( "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n" "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.2, 2.0, 0.0, 3.0, 4.0\n" "0.0, 1.0, 0.0\n")}; const auto result = parse_and_map(input); expect_diagnostic( result, "abaqus.semantic.duplicate_section_assignment", 14U); } } // namespace