diff --git a/src/fesa/io/abaqus/domain_mapping_context.cpp b/src/fesa/io/abaqus/domain_mapping_context.cpp index 7a7afe9..9a6c235 100644 --- a/src/fesa/io/abaqus/domain_mapping_context.cpp +++ b/src/fesa/io/abaqus/domain_mapping_context.cpp @@ -916,7 +916,8 @@ class DomainMappingContext::Impl { "BEAM GENERAL SECTION follows the part mesh and optional sets."); return; } - if (model_element_family_ == ElementFamily::kShell) { + if (model_element_family_ == ElementFamily::kShell || + model_element_family_ == ElementFamily::kThermalShell) { InputFailure( "unsupported-mixed-element-model", block.location, block.canonical_name, "", @@ -1006,7 +1007,14 @@ class DomainMappingContext::Impl { } void ParseElements(const KeywordBlock& block, RawPart& part) { - if (!ValidateParameters(block, {"TYPE"})) { + const auto* type_parameter = Parameter(block, "TYPE"); + const bool thermal_block = + type_parameter && type_parameter->value && + AsciiCaseInsensitiveEquals(*type_parameter->value, "DS4"); + if (!ValidateParameters(block, + thermal_block + ? std::vector{"TYPE", "ELSET"} + : std::vector{"TYPE"})) { return; } const auto* type = RequiredParameterValue(block, "TYPE"); @@ -1047,6 +1055,17 @@ class DomainMappingContext::Impl { return; } model_element_family_ = element_family; + std::optional inline_set; + if (Parameter(block, "ELSET")) { + const auto* name = RequiredParameterValue(block, "ELSET"); + if (name == nullptr) return; + if (ContainsSetName(part.element_sets, *name)) { + InputFailure("duplicate-entity", block.location, block.canonical_name, + *name, "Element-set names are unique within their part."); + return; + } + inline_set = RawSet{*name, {}, block.location}; + } if (block.data.empty()) { InputFailure(element_family == ElementFamily::kShell ? "invalid-shell-connectivity" @@ -1105,8 +1124,12 @@ class DomainMappingContext::Impl { "Element labels are unique within a part."); return; } + if (inline_set) inline_set->members.push_back(element.label); part.elements.push_back(std::move(element)); } + // Inline membership follows the block's source rows and uses the same + // part-set namespace and section resolution as an explicit ELSET. + if (inline_set) part.element_sets.push_back(std::move(*inline_set)); } void ParseShellSection(const KeywordBlock& block, RawPart& part) { diff --git a/tests/unit/io/abaqus/thermal_domain_test.cpp b/tests/unit/io/abaqus/thermal_domain_test.cpp index e0588e6..118a211 100644 --- a/tests/unit/io/abaqus/thermal_domain_test.cpp +++ b/tests/unit/io/abaqus/thermal_domain_test.cpp @@ -936,4 +936,115 @@ TEST(ThermalDomainMapping, RejectsMixedElementsAndWrongThermalMaterialScopes) { } } +TEST(ThermalDomainMapping, InlineElementSetResolvesSectionCaseInsensitively) { + auto deck = Replace(ThermalDeck(), "*Element, type=DS4", + "*Element, type=ds4, elset=inlineShells"); + deck = Replace(deck, "*Elset, elset=E\n1\n", ""); + deck = Replace(deck, "*Shell Section, elset=E", + "*Shell Section, elset=INLINESHELLS"); + const TemporaryInput input{deck}; + const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); + ASSERT_TRUE(parsed.HasValue()); + const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value()); + ASSERT_TRUE(result.HasValue()) + << result.GetStatus().Diagnostics().front().message; + const auto& domain = result.Value(); + ASSERT_EQ(domain.ElementSets().size(), 1U); + const auto& set = domain.ElementSets()[0]; + EXPECT_EQ(set.name, "inlineShells"); + EXPECT_EQ(set.instance_name, "I"); + EXPECT_EQ(set.element_indices, std::vector{0U}); + EXPECT_EQ(set.location.file, input.Path()); + EXPECT_EQ(set.location.line, LineOf(deck, "*Element")); + ASSERT_EQ(domain.ThermalElements().Size(), 1U); + EXPECT_EQ(domain.ThermalElements()[0].ThermalSectionIndex(), 0U); + EXPECT_EQ(domain.ThermalSections()[0].name, "INLINESHELLS"); + EXPECT_EQ(domain.ThermalSections()[0].location.line, + LineOf(deck, "*Shell Section")); +} + +TEST(ThermalDomainMapping, InlineElementSetPreservesElementRowOrder) { + auto deck = Replace(ThermalDeck(), "*Element, type=DS4\n1, 1, 2, 3, 4", + "5,2,0,0\n6,3,0,0\n7,3,1,0\n8,2,1,0\n" + "*Element, type=DS4, elset=E\n7,1,2,3,4\n3,5,6,7,8"); + deck = Replace(deck, "*Elset, elset=E\n1\n", ""); + const auto result = MapText(deck); + ASSERT_TRUE(result.HasValue()) + << result.GetStatus().Diagnostics().front().message; + const auto& domain = result.Value(); + ASSERT_EQ(domain.ElementSets().size(), 1U); + ASSERT_EQ(domain.ElementSets()[0].element_indices.size(), 2U); + EXPECT_EQ(domain.ElementSets()[0].element_indices, + (std::vector{0U, 1U})); + EXPECT_EQ(domain.ThermalElements()[0].SourceId().source_label, 7); + EXPECT_EQ(domain.ThermalElements()[1].SourceId().source_label, 3); + EXPECT_EQ(domain.ThermalElements()[0].ThermalSectionIndex(), 0U); + EXPECT_EQ(domain.ThermalElements()[1].ThermalSectionIndex(), 0U); +} + +TEST(ThermalDomainMapping, + InlineElementSetRejectsInvalidAndDuplicateDefinitions) { + auto deck = Replace(ThermalDeck(), "*Elset, elset=E\n1\n", ""); + for (const std::string option : + {"elset", "elset=", "elset=E, unknown=1", "elset=E, elset=e"}) { + SCOPED_TRACE(option); + const auto result = MapText( + Replace(deck, "*Element, type=DS4", "*Element, type=DS4, " + option)); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Diagnostics()[0].keyword, "ELEMENT"); + EXPECT_EQ(result.GetStatus().Diagnostics()[0].location.line, + LineOf(deck, "*Element")); + } + ExpectRejected(Replace(ThermalDeck(), "*Element, type=DS4", + "*Element, type=DS4, elset=e"), + "ELSET"); + deck = Replace(deck, "*Element, type=DS4", "*Element, type=DS4, elset=E"); + ExpectRejected( + Replace(deck, "*Shell Section", + "*Element, type=DS4, elset=e\n2,1,2,3,4\n*Shell Section"), + "ELEMENT"); + auto conflicting = + Replace(deck, "*Shell Section", "*Elset, elset=Other\n1\n*Shell Section"); + conflicting = + Replace(conflicting, "*End Part", + "*Shell Section, elset=Other, material=M\n.5\n*End Part"); + ExpectRejected(conflicting, "SHELL SECTION"); +} + +TEST(ThermalDomainMapping, RejectsBeamSectionAndBeamOnlyChildrenAtTheirSource) { + const std::string beam = + "*Beam General Section, elset=E, material=M, " + "section=GENERAL\n1,1,0,1,1\n1,0,0\n"; + for (const std::string suffix : + {"", "*Section Points\n0,0\n", "*Transverse Shear Stiffness\n1,1,1\n"}) { + const auto deck = + Replace(ThermalDeck(), "*End Part", beam + suffix + "*End Part"); + const auto result = MapText(deck); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Diagnostics()[0].keyword, + "BEAM GENERAL SECTION"); + EXPECT_EQ(result.GetStatus().Diagnostics()[0].location.line, + LineOf(deck, "*Beam General Section")); + } + ExpectRejected( + Replace(ThermalDeck(), "*End Part", "*Section Points\n0,0\n*End Part"), + "SECTION POINTS"); + ExpectRejected(Replace(ThermalDeck(), "*End Part", + "*Transverse Shear Stiffness\n1,1,1\n*End Part"), + "TRANSVERSE SHEAR STIFFNESS"); +} + +TEST(ThermalDomainMapping, RejectsMalformedDs4ConnectivityAndElementOptions) { + for (const std::string row : + {"1,1,2,3", "1,1,2,3,4,1", "1,1,2,3,3", "1,0,2,3,4", "1,-1,2,3,4", + "1,1.5,2,3,4", "1,1,2,3,99", "0,1,2,3,4", "1.5,1,2,3,4", + "1,1,2,3,4\n1,1,2,3,4"}) { + SCOPED_TRACE(row); + ExpectRejected(Replace(ThermalDeck(), "1, 1, 2, 3, 4", row), "ELEMENT"); + } + ExpectRejected(Replace(ThermalDeck(), "*Element, type=DS4", + "*Element, type=DS4, unknown=1"), + "ELEMENT"); +} + } // namespace