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
+357 -68
View File
@@ -48,12 +48,24 @@ struct RawNode {
};
struct RawElement {
enum class Type {
b33,
s4,
s4r
};
std::int64_t label;
std::string labelText;
std::array<std::int64_t, 2> nodeLabels;
Type type;
std::vector<std::int64_t> nodeLabels;
SourceLocation location;
};
enum class ElementFamily {
beam,
shell
};
struct RawSet {
std::string name;
std::vector<std::int64_t> members;
@@ -69,6 +81,13 @@ struct RawSection {
SourceLocation location;
};
struct RawShellSection {
std::string elementSetName;
std::string materialName;
double thickness;
SourceLocation location;
};
struct RawPart {
std::string name;
SourceLocation location;
@@ -77,6 +96,7 @@ struct RawPart {
std::vector<RawSet> nodeSets;
std::vector<RawSet> elementSets;
std::vector<RawSection> sections;
std::vector<RawShellSection> shellSections;
};
struct RawInstance {
@@ -551,7 +571,7 @@ private:
"Part names are unique under case-insensitive lookup.");
return;
}
parts_.push_back({*name, block.location, {}, {}, {}, {}, {}});
parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}});
currentPart_ = parts_.size() - 1U;
partElementsSeen_ = false;
partSetsSeen_ = false;
@@ -565,11 +585,12 @@ private:
return;
}
const RawPart& part = parts_[*currentPart_];
const bool shellPart = modelElementFamily_ == ElementFamily::shell;
if (part.nodes.empty() || part.elements.empty() ||
part.sections.empty()) {
(!shellPart && part.sections.empty())) {
invalidKeywordLocation(
block,
"A part closes only after node, element, and beam-section blocks.");
"A part closes only after node, element, and matching section blocks.");
return;
}
currentPart_.reset();
@@ -647,9 +668,33 @@ private:
"BEAM GENERAL SECTION follows the part mesh and optional sets.");
return;
}
if (modelElementFamily_ == ElementFamily::shell) {
inputFailure(
"unsupported-mixed-element-model", block.location,
block.canonicalName, "",
"Beam-section semantics cannot be mixed with shell elements.");
return;
}
parseBeamSection(block, part);
partSectionsSeen_ = !failure_;
beamSectionContextActive_ = !failure_;
} else if (block.canonicalName == "SHELL SECTION") {
beamSectionContextActive_ = false;
if (!partElementsSeen_) {
invalidKeywordLocation(
block,
"SHELL SECTION follows the part mesh and optional sets.");
return;
}
if (modelElementFamily_ == ElementFamily::beam) {
inputFailure(
"unsupported-mixed-element-model", block.location,
block.canonicalName, "",
"Shell-section semantics cannot be mixed with beam elements.");
return;
}
parseShellSection(block, part);
partSectionsSeen_ = !failure_;
} else if (block.canonicalName == "SECTION POINTS") {
parseSectionPoints(block, part);
} else if (block.canonicalName == "TRANSVERSE SHEAR STIFFNESS") {
@@ -726,42 +771,91 @@ private:
if (type == nullptr) {
return;
}
if (!equalName(*type, "B33")) {
RawElement::Type elementType{};
ElementFamily elementFamily{};
std::size_t expectedFieldCount = 0U;
if (equalName(*type, "B33")) {
elementType = RawElement::Type::b33;
elementFamily = ElementFamily::beam;
expectedFieldCount = 3U;
} else if (equalName(*type, "S4")) {
elementType = RawElement::Type::s4;
elementFamily = ElementFamily::shell;
expectedFieldCount = 5U;
} else if (equalName(*type, "S4R")) {
elementType = RawElement::Type::s4r;
elementFamily = ElementFamily::shell;
expectedFieldCount = 5U;
} else {
inputFailure(
"unsupported-element-formulation",
block.location,
block.canonicalName,
*type,
"Only TYPE=B33 maps to the V0 Euler beam.");
"Only TYPE=B33, TYPE=S4, and TYPE=S4R belong to the approved element subsets.");
return;
}
if (modelElementFamily_ && *modelElementFamily_ != elementFamily) {
inputFailure(
"unsupported-mixed-element-model",
block.location,
block.canonicalName,
*type,
"Beam and shell elements cannot be mixed in one model.");
return;
}
modelElementFamily_ = elementFamily;
if (block.data.empty()) {
inputFailure(
"invalid-data-arity", block.location, block.canonicalName,
"", "ELEMENT requires at least one three-field row.");
elementFamily == ElementFamily::shell
? "invalid-shell-connectivity"
: "invalid-data-arity",
block.location, block.canonicalName, "",
"ELEMENT requires at least one row with the approved connectivity arity.");
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != 3U) {
if (row.fields.size() != expectedFieldCount) {
inputFailure(
"invalid-data-arity", row.location, block.canonicalName,
"", "B33 rows require label, node 1, node 2.");
elementFamily == ElementFamily::shell
? "invalid-shell-connectivity"
: "invalid-data-arity",
row.location, block.canonicalName, "",
elementFamily == ElementFamily::shell
? "S4 and S4R rows require a label and exactly four nodes."
: "B33 rows require label, node 1, node 2.");
return;
}
RawElement element{};
element.labelText = row.fields[0];
element.type = elementType;
element.location = row.location;
if (!parseInteger(
row.fields[0], element.label, row.location,
block.canonicalName, true) ||
!parseInteger(
row.fields[1], element.nodeLabels[0], row.location,
block.canonicalName, true) ||
!parseInteger(
row.fields[2], element.nodeLabels[1], row.location,
block.canonicalName, true)) {
return;
}
element.nodeLabels.reserve(expectedFieldCount - 1U);
for (std::size_t field = 1U; field < expectedFieldCount; ++field) {
std::int64_t nodeLabel = 0;
if (!parseInteger(
row.fields[field], nodeLabel, row.location,
block.canonicalName, true)) {
return;
}
element.nodeLabels.push_back(nodeLabel);
}
if (elementFamily == ElementFamily::shell) {
const std::set<std::int64_t> distinctNodes{
element.nodeLabels.begin(), element.nodeLabels.end()};
if (distinctNodes.size() != element.nodeLabels.size()) {
inputFailure(
"invalid-shell-connectivity", row.location,
block.canonicalName, element.labelText,
"Shell connectivity requires four distinct source nodes.");
return;
}
}
if (std::any_of(
part.elements.begin(), part.elements.end(),
[&element](const RawElement& existing) {
@@ -777,6 +871,62 @@ private:
}
}
void parseShellSection(const KeywordBlock& block, RawPart& part) {
for (const auto& candidate : block.parameters) {
if (candidate.name != "ELSET" && candidate.name != "MATERIAL") {
inputFailure(
"unsupported-shell-section-option", block.location,
block.canonicalName, candidate.name,
"The shell-section option is outside the centered single-layer subset.");
return;
}
}
if (!validateParameters(block, {"ELSET", "MATERIAL"})) {
return;
}
const auto* elementSet = requiredParameterValue(block, "ELSET");
const auto* material = requiredParameterValue(block, "MATERIAL");
if (elementSet == nullptr || material == nullptr) {
return;
}
if (block.data.size() != 1U || block.data[0].fields.empty() ||
block.data[0].fields.size() > 2U) {
inputFailure(
"unsupported-shell-section-option", block.location,
block.canonicalName, *elementSet,
"SHELL SECTION requires one thickness row with one optional integration-point field.");
return;
}
double thickness = 0.0;
if (!parseModelDouble(
block.data[0].fields[0], thickness,
block.data[0].location, block.canonicalName,
"invalid-shell-thickness",
"Shell thickness must be finite and positive.")) {
return;
}
if (!(thickness > 0.0)) {
modelFailure(
"invalid-shell-thickness", block.data[0].location,
block.canonicalName, *elementSet,
"Shell thickness must be finite and positive.");
return;
}
if (block.data[0].fields.size() == 2U) {
std::int64_t ignoredIntegrationPoints = 0;
if (!tryPositiveInteger(
block.data[0].fields[1], ignoredIntegrationPoints)) {
inputFailure(
"unsupported-shell-section-option", block.data[0].location,
block.canonicalName, block.data[0].fields[1],
"The optional shell integration-point field must be a positive integer.");
return;
}
}
part.shellSections.push_back(
{*elementSet, *material, thickness, block.location});
}
bool parseSetMembers(
const KeywordBlock& block,
bool generate,
@@ -1208,12 +1358,16 @@ private:
if (!parseModelDouble(
block.data[0].fields[0], material.youngsModulus,
block.data[0].location, block.canonicalName,
"invalid-beam-property",
modelElementFamily_ == ElementFamily::shell
? "invalid-shell-material"
: "invalid-beam-property",
"Elastic material values must be finite.") ||
!parseModelDouble(
block.data[0].fields[1], material.poissonRatio,
block.data[0].location, block.canonicalName,
"invalid-beam-property",
modelElementFamily_ == ElementFamily::shell
? "invalid-shell-material"
: "invalid-beam-property",
"Elastic material values must be finite.")) {
return;
}
@@ -1346,10 +1500,17 @@ private:
return;
}
if (!equalName(*nlgeom->value, "NO")) {
modelFailure(
"unsupported-nonlinear-geometry", block.location,
block.canonicalName, *nlgeom->value,
"Only absent NLGEOM or NLGEOM=NO is supported.");
if (modelElementFamily_ == ElementFamily::shell) {
inputFailure(
"unsupported-nonlinear-geometry", block.location,
block.canonicalName, *nlgeom->value,
"Only absent NLGEOM or NLGEOM=NO is supported.");
} else {
modelFailure(
"unsupported-nonlinear-geometry", block.location,
block.canonicalName, *nlgeom->value,
"Only absent NLGEOM or NLGEOM=NO is supported.");
}
return;
}
}
@@ -1517,6 +1678,14 @@ private:
}
void rejectUnknown(const KeywordBlock& block) {
if (block.canonicalName == "DLOAD" &&
modelElementFamily_ == ElementFamily::shell) {
inputFailure(
"unsupported-distributed-load", block.location,
block.canonicalName, "",
"Distributed, pressure, gravity, body, edge, and follower loads are unsupported.");
return;
}
inputFailure(
"unsupported-keyword", block.location,
block.canonicalName, "",
@@ -1615,6 +1784,23 @@ private:
"A material must own exactly one ELASTIC row.");
return;
}
if (modelElementFamily_ == ElementFamily::shell) {
if (!(material.youngsModulus > 0.0) ||
!(material.poissonRatio > -1.0) ||
!(material.poissonRatio < 0.5)) {
modelFailure(
"invalid-shell-material", material.elasticLocation,
"ELASTIC", material.name,
"Shell isotropic elasticity requires E>0 and -1<nu<0.5.");
return;
}
definition_.materials.push_back({
material.name,
material.youngsModulus,
material.poissonRatio,
material.location});
continue;
}
const double denominator = 2.0 * (1.0 + material.poissonRatio);
const double shearModulus = material.youngsModulus / denominator;
// The approved ledger deliberately validates derived G, not an
@@ -1637,14 +1823,16 @@ private:
void finalizePartsAndSections() {
partSectionAssignments_.resize(parts_.size());
partShellSectionAssignments_.resize(parts_.size());
const bool shellModel = modelElementFamily_ == ElementFamily::shell;
for (std::size_t partIndex = 0U; partIndex < parts_.size(); ++partIndex) {
const RawPart& part = parts_[partIndex];
if (part.nodes.empty() || part.elements.empty() ||
part.sections.empty()) {
(!shellModel && part.sections.empty())) {
inputFailure(
"invalid-model-cardinality", part.location,
"PART", part.name,
"A part requires nodes, B33 elements, and a beam section.");
"A part requires nodes, elements, and its approved section form.");
return;
}
@@ -1656,13 +1844,16 @@ private:
}
for (const auto& element : part.elements) {
partDefinition.elementSourceLabels.push_back(element.label);
if (findNode(part, element.nodeLabels[0]) == nullptr ||
findNode(part, element.nodeLabels[1]) == nullptr) {
inputFailure(
"unresolved-reference", element.location,
"ELEMENT", element.labelText,
"Element connectivity must resolve within its part.");
return;
for (const auto nodeLabel : element.nodeLabels) {
if (findNode(part, nodeLabel) == nullptr) {
inputFailure(
shellModel
? "invalid-shell-connectivity"
: "unresolved-reference",
element.location, "ELEMENT", element.labelText,
"Element connectivity must resolve within its part.");
return;
}
}
}
for (const auto& set : part.nodeSets) {
@@ -1691,6 +1882,52 @@ private:
}
definition_.parts.push_back(std::move(partDefinition));
if (shellModel) {
auto& assignments = partShellSectionAssignments_[partIndex];
for (const auto& section : part.shellSections) {
const auto* elementSet = findSet(
part.elementSets, section.elementSetName);
const auto materialIndex = findMaterialIndex(section.materialName);
if (elementSet == nullptr || !materialIndex) {
inputFailure(
"unresolved-shell-section", section.location,
"SHELL SECTION", section.elementSetName,
"Shell section ELSET and MATERIAL references must resolve.");
return;
}
const EntityIndex sectionIndex =
static_cast<EntityIndex>(definition_.shellSections.size());
definition_.shellSections.push_back({
section.elementSetName,
section.thickness,
*materialIndex,
section.location});
for (const auto elementLabel : elementSet->members) {
if (!assignments.emplace(
elementLabel,
std::make_pair(
sectionIndex, *materialIndex)).second) {
inputFailure(
"invalid-shell-section-assignment",
section.location, "SHELL SECTION",
std::to_string(elementLabel),
"A shell element cannot receive multiple section assignments.");
return;
}
}
}
for (const auto& element : part.elements) {
if (assignments.find(element.label) == assignments.end()) {
inputFailure(
"invalid-shell-section-assignment", element.location,
"ELEMENT", element.labelText,
"Every shell element requires exactly one resolved section assignment.");
return;
}
}
continue;
}
auto& assignments = partSectionAssignments_[partIndex];
for (const auto& section : part.sections) {
const auto* elementSet = findSet(
@@ -1873,44 +2110,92 @@ private:
instance.nodeMappings.push_back({rawNode.label, index});
}
const auto& assignments =
partSectionAssignments_[partIndex(*part)];
for (const auto& rawElement : part->elements) {
const auto first = nodeIndices.find(rawElement.nodeLabels[0]);
const auto second = nodeIndices.find(rawElement.nodeLabels[1]);
const auto assignment = assignments.find(rawElement.label);
if (first == nodeIndices.end() || second == nodeIndices.end() ||
assignment == assignments.end()) {
inputFailure(
"unresolved-reference", rawElement.location,
"ELEMENT", rawElement.labelText,
"Expanded connectivity and section assignment must resolve.");
return;
EntityIndex index = 0U;
if (rawElement.type == RawElement::Type::b33) {
const auto first = nodeIndices.find(rawElement.nodeLabels[0]);
const auto second = nodeIndices.find(rawElement.nodeLabels[1]);
const auto& assignments =
partSectionAssignments_[partIndex(*part)];
const auto assignment = assignments.find(rawElement.label);
if (first == nodeIndices.end() || second == nodeIndices.end() ||
assignment == assignments.end()) {
inputFailure(
"unresolved-reference", rawElement.location,
"ELEMENT", rawElement.labelText,
"Expanded connectivity and section assignment must resolve.");
return;
}
if (definition_.elements.size() >
std::numeric_limits<EntityIndex>::max()) {
inputFailure(
"entity-index-overflow", rawElement.location,
"ELEMENT", rawElement.labelText,
"Expanded element count exceeds EntityIndex capacity.");
return;
}
index = static_cast<EntityIndex>(definition_.elements.size());
const auto& section =
definition_.sections[assignment->second.first];
if (!validateGeometry(
rawElement,
definition_.nodes[first->second],
definition_.nodes[second->second],
section)) {
return;
}
definition_.elements.push_back({
{rawInstance.name, rawElement.label, rawElement.labelText},
{first->second, second->second},
assignment->second.second,
assignment->second.first,
rawElement.location});
} else {
std::array<EntityIndex, 4> connectedNodes{};
for (std::size_t node = 0U;
node < connectedNodes.size();
++node) {
const auto found =
nodeIndices.find(rawElement.nodeLabels[node]);
if (found == nodeIndices.end()) {
inputFailure(
"invalid-shell-connectivity", rawElement.location,
"ELEMENT", rawElement.labelText,
"Expanded shell connectivity must resolve.");
return;
}
connectedNodes[node] = found->second;
}
const auto& assignments =
partShellSectionAssignments_[partIndex(*part)];
const auto assignment = assignments.find(rawElement.label);
if (assignment == assignments.end()) {
inputFailure(
"invalid-shell-section-assignment",
rawElement.location, "ELEMENT", rawElement.labelText,
"Expanded shell section assignment must resolve exactly once.");
return;
}
if (definition_.shellElements.size() >
std::numeric_limits<EntityIndex>::max()) {
inputFailure(
"entity-index-overflow", rawElement.location,
"ELEMENT", rawElement.labelText,
"Expanded shell element count exceeds EntityIndex capacity.");
return;
}
index =
static_cast<EntityIndex>(definition_.shellElements.size());
definition_.shellElements.push_back({
{rawInstance.name, rawElement.label, rawElement.labelText},
rawElement.type == RawElement::Type::s4
? ShellSourceElementType::s4
: ShellSourceElementType::s4r,
connectedNodes,
assignment->second.second,
assignment->second.first,
rawElement.location});
}
if (definition_.elements.size() >
std::numeric_limits<EntityIndex>::max()) {
inputFailure(
"entity-index-overflow", rawElement.location,
"ELEMENT", rawElement.labelText,
"Expanded element count exceeds EntityIndex capacity.");
return;
}
const EntityIndex index =
static_cast<EntityIndex>(definition_.elements.size());
const auto& section = definition_.sections[assignment->second.first];
if (!validateGeometry(
rawElement,
definition_.nodes[first->second],
definition_.nodes[second->second],
section)) {
return;
}
definition_.elements.push_back({
{rawInstance.name, rawElement.label, rawElement.labelText},
{first->second, second->second},
assignment->second.second,
assignment->second.first,
rawElement.location});
elementIndices.emplace(rawElement.label, index);
instance.elementMappings.push_back({rawElement.label, index});
}
@@ -2134,10 +2419,14 @@ private:
std::vector<std::map<
std::int64_t,
std::pair<EntityIndex, EntityIndex>>> partSectionAssignments_;
std::vector<std::map<
std::int64_t,
std::pair<EntityIndex, EntityIndex>>> partShellSectionAssignments_;
std::optional<std::size_t> currentPart_;
std::optional<std::size_t> pendingSection_;
std::optional<std::size_t> materialEligible_;
std::optional<ElementFamily> modelElementFamily_;
bool headingSeen_{false};
bool partElementsSeen_{false};
bool partSetsSeen_{false};
+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;