feat(linear-static-mitc4-shell): step 1 - shell-domain-mapping

This commit is contained in:
KOKO\Mimi
2026-08-12 19:13:23 +09:00
parent d2414fadcc
commit 45ab77b052
2 changed files with 588 additions and 68 deletions
+231
View File
@@ -128,6 +128,55 @@ Tip, 2, -1.
)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 includeNoOps) {
const std::string preprint = includeNoOps
? "*Preprint, echo=NO, model=NO, history=NO, contact=NO\n"
@@ -422,6 +471,188 @@ TEST(InpDomainMapping, NoOpAllowlistWarnsWithoutSemanticEffect) {
plain.value().steps()[0].loads.size());
}
// MITC4-MAP-001
TEST(InpDomainMapping, MapsS4AndS4rThroughOneMitc4Identity) {
auto result = mapText("mitc4-map-001", shellDeck());
ASSERT_TRUE(result.hasValue());
const fesa::Domain& domain = result.value();
EXPECT_TRUE(domain.elements().empty());
ASSERT_EQ(domain.shellElements().size(), 4U);
EXPECT_EQ(domain.shellElements()[0].sourceId.instanceName, "First");
EXPECT_EQ(domain.shellElements()[0].sourceId.sourceLabelText, "0010");
EXPECT_EQ(
domain.shellElements()[0].sourceType,
fesa::ShellSourceElementType::s4);
EXPECT_EQ(
domain.shellElements()[0].nodeIndices,
(std::array<fesa::EntityIndex, 4>{0U, 1U, 2U, 3U}));
EXPECT_EQ(domain.shellElements()[0].sectionIndex, 0U);
EXPECT_EQ(domain.shellElements()[0].materialIndex, 0U);
EXPECT_EQ(domain.shellElements()[1].sourceId.instanceName, "First");
EXPECT_EQ(
domain.shellElements()[1].sourceType,
fesa::ShellSourceElementType::s4r);
EXPECT_EQ(
domain.shellElements()[1].nodeIndices,
(std::array<fesa::EntityIndex, 4>{1U, 4U, 5U, 2U}));
EXPECT_EQ(domain.shellElements()[1].sectionIndex, 1U);
EXPECT_EQ(domain.shellElements()[1].materialIndex, 1U);
EXPECT_EQ(domain.shellElements()[2].sourceId.instanceName, "Second");
EXPECT_EQ(
domain.shellElements()[2].nodeIndices,
(std::array<fesa::EntityIndex, 4>{6U, 7U, 8U, 9U}));
EXPECT_EQ(domain.shellElements()[3].sourceId.instanceName, "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].materialIndex, 0U);
EXPECT_EQ(domain.shellSections()[1].name, "ShellS4R");
EXPECT_DOUBLE_EQ(domain.shellSections()[1].thickness, 0.2);
EXPECT_EQ(domain.shellSections()[1].materialIndex, 1U);
ASSERT_EQ(domain.steps().size(), 1U);
ASSERT_EQ(domain.steps()[0].boundaries.size(), 1U);
EXPECT_EQ(domain.steps()[0].boundaries[0].lastDof, 6);
ASSERT_EQ(domain.steps()[0].loads.size(), 1U);
EXPECT_EQ(domain.steps()[0].loads[0].dof, 6);
}
// MITC4-MAP-002
TEST(InpDomainMapping, RejectsInvalidShellAssignmentsAndProperties) {
struct InvalidCase {
std::string name;
std::string deck;
std::string expectedCode;
fesa::FailureCategory category;
};
const std::string base = shellDeck();
const std::vector<InvalidCase> cases{
{"unresolved-material",
replaceOnce(base, "material=Steel", "material=Missing"),
"unresolved-shell-section", fesa::FailureCategory::input},
{"unresolved-elset",
replaceOnce(base, "elset=ShellS4, material=Steel",
"elset=Missing, material=Steel"),
"unresolved-shell-section", fesa::FailureCategory::input},
{"missing-assignment",
replaceOnce(
base,
"*Shell Section, elset=ShellS4R, material=Aluminum\n0.2\n",
""),
"invalid-shell-section-assignment", fesa::FailureCategory::input},
{"conflicting-assignment",
replaceOnce(base, "elset=ShellS4R, material=Aluminum",
"elset=ShellS4, material=Aluminum"),
"invalid-shell-section-assignment", fesa::FailureCategory::input},
{"invalid-thickness",
replaceOnce(base, "0.2\n*End Part", "0.\n*End Part"),
"invalid-shell-thickness", fesa::FailureCategory::model},
{"invalid-material",
replaceOnce(base, "70000., 0.25", "70000., 0.5"),
"invalid-shell-material", fesa::FailureCategory::model}};
for (const auto& testCase : cases) {
SCOPED_TRACE(testCase.name);
auto result = mapText("mitc4-map-002-" + testCase.name, testCase.deck);
ASSERT_FALSE(result.hasValue());
EXPECT_EQ(result.status().failureCategory(), testCase.category);
ASSERT_NE(findDiagnostic(result.status(), testCase.expectedCode), nullptr);
}
}
// MITC4-MAP-003
TEST(InpDomainMapping, RejectsInvalidShellConnectivityOptionsAndMixedModels) {
struct InvalidCase {
std::string name;
std::string deck;
std::string expectedCode;
};
const std::string base = shellDeck();
const std::vector<InvalidCase> 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& testCase : cases) {
SCOPED_TRACE(testCase.name);
auto result = mapText("mitc4-map-003-" + testCase.name, testCase.deck);
ASSERT_FALSE(result.hasValue());
EXPECT_EQ(
result.status().failureCategory(),
fesa::FailureCategory::input);
ASSERT_NE(findDiagnostic(result.status(), testCase.expectedCode), nullptr);
}
}
// MITC4-MAP-004
TEST(InpDomainMapping, PreservesProcedureLoadAndOutputRequestBoundariesForShells) {
const std::string withNoOps = 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", withNoOps);
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 expectedCode;
};
const std::string base = shellDeck();
const std::vector<InvalidCase> 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& testCase : cases) {
SCOPED_TRACE(testCase.name);
auto result = mapText("mitc4-map-004-" + testCase.name, testCase.deck);
ASSERT_FALSE(result.hasValue());
EXPECT_EQ(
result.status().failureCategory(),
fesa::FailureCategory::input);
ASSERT_NE(findDiagnostic(result.status(), testCase.expectedCode), nullptr);
}
}
TEST(InpDomainMapping, RejectsUnsupportedAndInvalidPortfolio) {
struct InvalidCase {
std::string name;