#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_; }; std::filesystem::path fixture_path(std::string_view name) { return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" / name; } fesa::DomainBuildResult parse_and_map(const std::filesystem::path& path) { const auto parsed = fesa::parse_deck(path); if (!parsed.deck.has_value()) { return {std::nullopt, parsed.diagnostics}; } return fesa::map_deck_to_domain(*parsed.deck); } bool has_diagnostic( const fesa::DomainBuildResult& result, const std::string_view code) { return std::ranges::any_of( result.diagnostics, [code](const fesa::Diagnostic& diagnostic) { return diagnostic.code == code; }); } void expect_equivalent_analysis_data( const fesa::Domain& flat, const fesa::Domain& hierarchical) { ASSERT_EQ(flat.nodes().size(), hierarchical.nodes().size()); ASSERT_EQ(flat.beam_elements().size(), hierarchical.beam_elements().size()); ASSERT_EQ(flat.materials().size(), hierarchical.materials().size()); ASSERT_EQ(flat.sections().size(), hierarchical.sections().size()); for (std::size_t index = 0; index < flat.nodes().size(); ++index) { const auto& flat_node = flat.nodes()[index]; const auto& hierarchical_node = hierarchical.nodes()[index]; EXPECT_EQ(flat_node.id, hierarchical_node.id); EXPECT_DOUBLE_EQ(flat_node.position.x, hierarchical_node.position.x); EXPECT_DOUBLE_EQ(flat_node.position.y, hierarchical_node.position.y); EXPECT_DOUBLE_EQ(flat_node.position.z, hierarchical_node.position.z); } const auto& flat_element = flat.beam_elements().front(); const auto& hierarchical_element = hierarchical.beam_elements().front(); EXPECT_EQ(flat_element.id, hierarchical_element.id); EXPECT_EQ(flat_element.nodes, hierarchical_element.nodes); EXPECT_EQ(flat_element.material, hierarchical_element.material); EXPECT_EQ(flat_element.section, hierarchical_element.section); const auto& flat_material = flat.materials().front(); const auto& hierarchical_material = hierarchical.materials().front(); EXPECT_EQ(flat_material.id, hierarchical_material.id); EXPECT_EQ(flat_material.name, hierarchical_material.name); EXPECT_DOUBLE_EQ(flat_material.young, hierarchical_material.young); EXPECT_DOUBLE_EQ(flat_material.poisson, hierarchical_material.poisson); const auto& flat_section = flat.sections().front(); const auto& hierarchical_section = hierarchical.sections().front(); EXPECT_EQ(flat_section.id, hierarchical_section.id); EXPECT_EQ(flat_section.name, hierarchical_section.name); EXPECT_DOUBLE_EQ(flat_section.area, hierarchical_section.area); EXPECT_DOUBLE_EQ(flat_section.iy, hierarchical_section.iy); EXPECT_DOUBLE_EQ(flat_section.iz, hierarchical_section.iz); EXPECT_DOUBLE_EQ(flat_section.torsion_j, hierarchical_section.torsion_j); EXPECT_DOUBLE_EQ( flat_section.shear_area_y, hierarchical_section.shear_area_y); EXPECT_DOUBLE_EQ( flat_section.shear_area_z, hierarchical_section.shear_area_z); ASSERT_EQ( flat.step().prescribed_dofs.size(), hierarchical.step().prescribed_dofs.size()); for (std::size_t index = 0; index < flat.step().prescribed_dofs.size(); ++index) { const auto& flat_value = flat.step().prescribed_dofs[index]; const auto& hierarchical_value = hierarchical.step().prescribed_dofs[index]; EXPECT_EQ(flat_value.node, hierarchical_value.node); EXPECT_EQ(flat_value.dof, hierarchical_value.dof); EXPECT_DOUBLE_EQ(flat_value.value, hierarchical_value.value); } ASSERT_EQ(flat.step().nodal_loads.size(), 1U); ASSERT_EQ(hierarchical.step().nodal_loads.size(), 1U); EXPECT_EQ( flat.step().nodal_loads[0].node, hierarchical.step().nodal_loads[0].node); EXPECT_EQ( flat.step().nodal_loads[0].values, hierarchical.step().nodal_loads[0].values); } TEST(DeckToDomain, NormalizesFlatAndSingleInstanceDecksEquivalently) { const auto flat = parse_and_map(fixture_path("minimal_cantilever.inp")); const auto hierarchical = parse_and_map(fixture_path("minimal_part_instance_cantilever.inp")); ASSERT_TRUE(flat.domain.has_value()); ASSERT_TRUE(hierarchical.domain.has_value()); EXPECT_TRUE(flat.diagnostics.empty()); EXPECT_TRUE(hierarchical.diagnostics.empty()); expect_equivalent_analysis_data(*flat.domain, *hierarchical.domain); for (const auto& node : flat.domain->nodes()) { EXPECT_TRUE(node.origin.part_name.empty()); EXPECT_TRUE(node.origin.instance_name.empty()); } for (const auto& node : hierarchical.domain->nodes()) { EXPECT_EQ(node.origin.part_name, "BeamPart"); EXPECT_EQ(node.origin.instance_name, "Beam-1"); } EXPECT_EQ( hierarchical.domain->beam_elements()[0].origin, (fesa::EntityOrigin{"BeamPart", "Beam-1", 1})); const auto& section = hierarchical.domain->sections().front(); EXPECT_DOUBLE_EQ(section.shear_area_y, 5.0 * section.area / 6.0); EXPECT_DOUBLE_EQ(section.shear_area_z, 5.0 * section.area / 6.0); EXPECT_EQ( section.shear_source, fesa::ShearPropertySource::phase1_default); } TEST(ActiveInstance, ExcludesPartsNotReferencedByTheInstance) { const TemporaryDeck input{ "fesa-active-instance.inp", "*PART, NAME=Unused\n" "*NODE\n" "99, 9.0, 0.0, 0.0\n" "*END PART\n" "*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, ELSET=Beam\n" "1, 1, 2\n" "*ELSET, ELSET=Beam\n" "1\n" "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" "1.0, 1.0, 0.0, 1.0, 1.0\n" "0.0, 1.0, 0.0\n" "*END PART\n" "*ASSEMBLY, NAME=RootAssembly\n" "*INSTANCE, NAME=Beam-1, PART=BeamPart\n" "*END INSTANCE\n" "*END ASSEMBLY\n" "*MATERIAL, NAME=Steel\n" "*ELASTIC\n" "210000.0, 0.3\n" "*STEP, NAME=Load\n" "*STATIC\n" "*END STEP\n"}; const auto result = parse_and_map(input.path()); ASSERT_TRUE(result.domain.has_value()); ASSERT_EQ(result.domain->nodes().size(), 2U); EXPECT_EQ(result.domain->nodes()[0].origin.part_name, "BeamPart"); EXPECT_EQ(result.domain->nodes()[1].origin.part_name, "BeamPart"); } TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) { const TemporaryDeck input{ "fesa-instance-transform.inp", "*PART, NAME=BeamPart\n" "*END PART\n" "*ASSEMBLY, NAME=RootAssembly\n" "*INSTANCE, NAME=Beam-1, PART=BeamPart\n" "1.0, 2.0, 3.0\n" "*END INSTANCE\n" "*END ASSEMBLY\n"}; const auto result = parse_and_map(input.path()); EXPECT_FALSE(result.domain.has_value()); ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.instance_transform")); ASSERT_FALSE(result.diagnostics.empty()); ASSERT_TRUE(result.diagnostics.front().source.has_value()); EXPECT_EQ(result.diagnostics.front().source->line, 4U); } TEST(ActiveInstance, RejectsMissingPartReferenceWithSourceDiagnostic) { const TemporaryDeck input{ "fesa-missing-part.inp", "*PART, NAME=OtherPart\n" "*END PART\n" "*ASSEMBLY, NAME=RootAssembly\n" "*INSTANCE, NAME=Beam-1, PART=MissingPart\n" "*END INSTANCE\n" "*END ASSEMBLY\n"}; const auto result = parse_and_map(input.path()); EXPECT_FALSE(result.domain.has_value()); ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.missing_part")); ASSERT_FALSE(result.diagnostics.empty()); ASSERT_TRUE(result.diagnostics.front().source.has_value()); EXPECT_EQ(result.diagnostics.front().source->line, 4U); } TEST(ActiveInstance, RejectsMultipleInstances) { const TemporaryDeck input{ "fesa-multiple-instances.inp", "*PART, NAME=BeamPart\n" "*END PART\n" "*ASSEMBLY, NAME=RootAssembly\n" "*INSTANCE, NAME=Beam-1, PART=BeamPart\n" "*END INSTANCE\n" "*INSTANCE, NAME=Beam-2, PART=BeamPart\n" "*END INSTANCE\n" "*END ASSEMBLY\n"}; const auto result = parse_and_map(input.path()); EXPECT_FALSE(result.domain.has_value()); const auto diagnostic = std::ranges::find( result.diagnostics, std::string_view{"abaqus.semantic.instance_count"}, &fesa::Diagnostic::code); ASSERT_NE(diagnostic, result.diagnostics.end()); ASSERT_TRUE(diagnostic->source.has_value()); EXPECT_EQ(diagnostic->source->line, 6U); } } // namespace