feat(linear-static-mitc4-shell): step 1 - shell-domain-mapping
This commit is contained in:
@@ -48,12 +48,24 @@ struct RawNode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct RawElement {
|
struct RawElement {
|
||||||
|
enum class Type {
|
||||||
|
b33,
|
||||||
|
s4,
|
||||||
|
s4r
|
||||||
|
};
|
||||||
|
|
||||||
std::int64_t label;
|
std::int64_t label;
|
||||||
std::string labelText;
|
std::string labelText;
|
||||||
std::array<std::int64_t, 2> nodeLabels;
|
Type type;
|
||||||
|
std::vector<std::int64_t> nodeLabels;
|
||||||
SourceLocation location;
|
SourceLocation location;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class ElementFamily {
|
||||||
|
beam,
|
||||||
|
shell
|
||||||
|
};
|
||||||
|
|
||||||
struct RawSet {
|
struct RawSet {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::vector<std::int64_t> members;
|
std::vector<std::int64_t> members;
|
||||||
@@ -69,6 +81,13 @@ struct RawSection {
|
|||||||
SourceLocation location;
|
SourceLocation location;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RawShellSection {
|
||||||
|
std::string elementSetName;
|
||||||
|
std::string materialName;
|
||||||
|
double thickness;
|
||||||
|
SourceLocation location;
|
||||||
|
};
|
||||||
|
|
||||||
struct RawPart {
|
struct RawPart {
|
||||||
std::string name;
|
std::string name;
|
||||||
SourceLocation location;
|
SourceLocation location;
|
||||||
@@ -77,6 +96,7 @@ struct RawPart {
|
|||||||
std::vector<RawSet> nodeSets;
|
std::vector<RawSet> nodeSets;
|
||||||
std::vector<RawSet> elementSets;
|
std::vector<RawSet> elementSets;
|
||||||
std::vector<RawSection> sections;
|
std::vector<RawSection> sections;
|
||||||
|
std::vector<RawShellSection> shellSections;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RawInstance {
|
struct RawInstance {
|
||||||
@@ -551,7 +571,7 @@ private:
|
|||||||
"Part names are unique under case-insensitive lookup.");
|
"Part names are unique under case-insensitive lookup.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parts_.push_back({*name, block.location, {}, {}, {}, {}, {}});
|
parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}});
|
||||||
currentPart_ = parts_.size() - 1U;
|
currentPart_ = parts_.size() - 1U;
|
||||||
partElementsSeen_ = false;
|
partElementsSeen_ = false;
|
||||||
partSetsSeen_ = false;
|
partSetsSeen_ = false;
|
||||||
@@ -565,11 +585,12 @@ private:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const RawPart& part = parts_[*currentPart_];
|
const RawPart& part = parts_[*currentPart_];
|
||||||
|
const bool shellPart = modelElementFamily_ == ElementFamily::shell;
|
||||||
if (part.nodes.empty() || part.elements.empty() ||
|
if (part.nodes.empty() || part.elements.empty() ||
|
||||||
part.sections.empty()) {
|
(!shellPart && part.sections.empty())) {
|
||||||
invalidKeywordLocation(
|
invalidKeywordLocation(
|
||||||
block,
|
block,
|
||||||
"A part closes only after node, element, and beam-section blocks.");
|
"A part closes only after node, element, and matching section blocks.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentPart_.reset();
|
currentPart_.reset();
|
||||||
@@ -647,9 +668,33 @@ private:
|
|||||||
"BEAM GENERAL SECTION follows the part mesh and optional sets.");
|
"BEAM GENERAL SECTION follows the part mesh and optional sets.");
|
||||||
return;
|
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);
|
parseBeamSection(block, part);
|
||||||
partSectionsSeen_ = !failure_;
|
partSectionsSeen_ = !failure_;
|
||||||
beamSectionContextActive_ = !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") {
|
} else if (block.canonicalName == "SECTION POINTS") {
|
||||||
parseSectionPoints(block, part);
|
parseSectionPoints(block, part);
|
||||||
} else if (block.canonicalName == "TRANSVERSE SHEAR STIFFNESS") {
|
} else if (block.canonicalName == "TRANSVERSE SHEAR STIFFNESS") {
|
||||||
@@ -726,42 +771,91 @@ private:
|
|||||||
if (type == nullptr) {
|
if (type == nullptr) {
|
||||||
return;
|
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(
|
inputFailure(
|
||||||
"unsupported-element-formulation",
|
"unsupported-element-formulation",
|
||||||
block.location,
|
block.location,
|
||||||
block.canonicalName,
|
block.canonicalName,
|
||||||
*type,
|
*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;
|
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()) {
|
if (block.data.empty()) {
|
||||||
inputFailure(
|
inputFailure(
|
||||||
"invalid-data-arity", block.location, block.canonicalName,
|
elementFamily == ElementFamily::shell
|
||||||
"", "ELEMENT requires at least one three-field row.");
|
? "invalid-shell-connectivity"
|
||||||
|
: "invalid-data-arity",
|
||||||
|
block.location, block.canonicalName, "",
|
||||||
|
"ELEMENT requires at least one row with the approved connectivity arity.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const auto& row : block.data) {
|
for (const auto& row : block.data) {
|
||||||
if (row.fields.size() != 3U) {
|
if (row.fields.size() != expectedFieldCount) {
|
||||||
inputFailure(
|
inputFailure(
|
||||||
"invalid-data-arity", row.location, block.canonicalName,
|
elementFamily == ElementFamily::shell
|
||||||
"", "B33 rows require label, node 1, node 2.");
|
? "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;
|
return;
|
||||||
}
|
}
|
||||||
RawElement element{};
|
RawElement element{};
|
||||||
element.labelText = row.fields[0];
|
element.labelText = row.fields[0];
|
||||||
|
element.type = elementType;
|
||||||
element.location = row.location;
|
element.location = row.location;
|
||||||
if (!parseInteger(
|
if (!parseInteger(
|
||||||
row.fields[0], element.label, row.location,
|
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)) {
|
block.canonicalName, true)) {
|
||||||
return;
|
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(
|
if (std::any_of(
|
||||||
part.elements.begin(), part.elements.end(),
|
part.elements.begin(), part.elements.end(),
|
||||||
[&element](const RawElement& existing) {
|
[&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(
|
bool parseSetMembers(
|
||||||
const KeywordBlock& block,
|
const KeywordBlock& block,
|
||||||
bool generate,
|
bool generate,
|
||||||
@@ -1208,12 +1358,16 @@ private:
|
|||||||
if (!parseModelDouble(
|
if (!parseModelDouble(
|
||||||
block.data[0].fields[0], material.youngsModulus,
|
block.data[0].fields[0], material.youngsModulus,
|
||||||
block.data[0].location, block.canonicalName,
|
block.data[0].location, block.canonicalName,
|
||||||
"invalid-beam-property",
|
modelElementFamily_ == ElementFamily::shell
|
||||||
|
? "invalid-shell-material"
|
||||||
|
: "invalid-beam-property",
|
||||||
"Elastic material values must be finite.") ||
|
"Elastic material values must be finite.") ||
|
||||||
!parseModelDouble(
|
!parseModelDouble(
|
||||||
block.data[0].fields[1], material.poissonRatio,
|
block.data[0].fields[1], material.poissonRatio,
|
||||||
block.data[0].location, block.canonicalName,
|
block.data[0].location, block.canonicalName,
|
||||||
"invalid-beam-property",
|
modelElementFamily_ == ElementFamily::shell
|
||||||
|
? "invalid-shell-material"
|
||||||
|
: "invalid-beam-property",
|
||||||
"Elastic material values must be finite.")) {
|
"Elastic material values must be finite.")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1346,10 +1500,17 @@ private:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!equalName(*nlgeom->value, "NO")) {
|
if (!equalName(*nlgeom->value, "NO")) {
|
||||||
modelFailure(
|
if (modelElementFamily_ == ElementFamily::shell) {
|
||||||
"unsupported-nonlinear-geometry", block.location,
|
inputFailure(
|
||||||
block.canonicalName, *nlgeom->value,
|
"unsupported-nonlinear-geometry", block.location,
|
||||||
"Only absent NLGEOM or NLGEOM=NO is supported.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1517,6 +1678,14 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void rejectUnknown(const KeywordBlock& block) {
|
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(
|
inputFailure(
|
||||||
"unsupported-keyword", block.location,
|
"unsupported-keyword", block.location,
|
||||||
block.canonicalName, "",
|
block.canonicalName, "",
|
||||||
@@ -1615,6 +1784,23 @@ private:
|
|||||||
"A material must own exactly one ELASTIC row.");
|
"A material must own exactly one ELASTIC row.");
|
||||||
return;
|
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 denominator = 2.0 * (1.0 + material.poissonRatio);
|
||||||
const double shearModulus = material.youngsModulus / denominator;
|
const double shearModulus = material.youngsModulus / denominator;
|
||||||
// The approved ledger deliberately validates derived G, not an
|
// The approved ledger deliberately validates derived G, not an
|
||||||
@@ -1637,14 +1823,16 @@ private:
|
|||||||
|
|
||||||
void finalizePartsAndSections() {
|
void finalizePartsAndSections() {
|
||||||
partSectionAssignments_.resize(parts_.size());
|
partSectionAssignments_.resize(parts_.size());
|
||||||
|
partShellSectionAssignments_.resize(parts_.size());
|
||||||
|
const bool shellModel = modelElementFamily_ == ElementFamily::shell;
|
||||||
for (std::size_t partIndex = 0U; partIndex < parts_.size(); ++partIndex) {
|
for (std::size_t partIndex = 0U; partIndex < parts_.size(); ++partIndex) {
|
||||||
const RawPart& part = parts_[partIndex];
|
const RawPart& part = parts_[partIndex];
|
||||||
if (part.nodes.empty() || part.elements.empty() ||
|
if (part.nodes.empty() || part.elements.empty() ||
|
||||||
part.sections.empty()) {
|
(!shellModel && part.sections.empty())) {
|
||||||
inputFailure(
|
inputFailure(
|
||||||
"invalid-model-cardinality", part.location,
|
"invalid-model-cardinality", part.location,
|
||||||
"PART", part.name,
|
"PART", part.name,
|
||||||
"A part requires nodes, B33 elements, and a beam section.");
|
"A part requires nodes, elements, and its approved section form.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1656,13 +1844,16 @@ private:
|
|||||||
}
|
}
|
||||||
for (const auto& element : part.elements) {
|
for (const auto& element : part.elements) {
|
||||||
partDefinition.elementSourceLabels.push_back(element.label);
|
partDefinition.elementSourceLabels.push_back(element.label);
|
||||||
if (findNode(part, element.nodeLabels[0]) == nullptr ||
|
for (const auto nodeLabel : element.nodeLabels) {
|
||||||
findNode(part, element.nodeLabels[1]) == nullptr) {
|
if (findNode(part, nodeLabel) == nullptr) {
|
||||||
inputFailure(
|
inputFailure(
|
||||||
"unresolved-reference", element.location,
|
shellModel
|
||||||
"ELEMENT", element.labelText,
|
? "invalid-shell-connectivity"
|
||||||
"Element connectivity must resolve within its part.");
|
: "unresolved-reference",
|
||||||
return;
|
element.location, "ELEMENT", element.labelText,
|
||||||
|
"Element connectivity must resolve within its part.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const auto& set : part.nodeSets) {
|
for (const auto& set : part.nodeSets) {
|
||||||
@@ -1691,6 +1882,52 @@ private:
|
|||||||
}
|
}
|
||||||
definition_.parts.push_back(std::move(partDefinition));
|
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];
|
auto& assignments = partSectionAssignments_[partIndex];
|
||||||
for (const auto& section : part.sections) {
|
for (const auto& section : part.sections) {
|
||||||
const auto* elementSet = findSet(
|
const auto* elementSet = findSet(
|
||||||
@@ -1873,44 +2110,92 @@ private:
|
|||||||
instance.nodeMappings.push_back({rawNode.label, index});
|
instance.nodeMappings.push_back({rawNode.label, index});
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& assignments =
|
|
||||||
partSectionAssignments_[partIndex(*part)];
|
|
||||||
for (const auto& rawElement : part->elements) {
|
for (const auto& rawElement : part->elements) {
|
||||||
const auto first = nodeIndices.find(rawElement.nodeLabels[0]);
|
EntityIndex index = 0U;
|
||||||
const auto second = nodeIndices.find(rawElement.nodeLabels[1]);
|
if (rawElement.type == RawElement::Type::b33) {
|
||||||
const auto assignment = assignments.find(rawElement.label);
|
const auto first = nodeIndices.find(rawElement.nodeLabels[0]);
|
||||||
if (first == nodeIndices.end() || second == nodeIndices.end() ||
|
const auto second = nodeIndices.find(rawElement.nodeLabels[1]);
|
||||||
assignment == assignments.end()) {
|
const auto& assignments =
|
||||||
inputFailure(
|
partSectionAssignments_[partIndex(*part)];
|
||||||
"unresolved-reference", rawElement.location,
|
const auto assignment = assignments.find(rawElement.label);
|
||||||
"ELEMENT", rawElement.labelText,
|
if (first == nodeIndices.end() || second == nodeIndices.end() ||
|
||||||
"Expanded connectivity and section assignment must resolve.");
|
assignment == assignments.end()) {
|
||||||
return;
|
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);
|
elementIndices.emplace(rawElement.label, index);
|
||||||
instance.elementMappings.push_back({rawElement.label, index});
|
instance.elementMappings.push_back({rawElement.label, index});
|
||||||
}
|
}
|
||||||
@@ -2134,10 +2419,14 @@ private:
|
|||||||
std::vector<std::map<
|
std::vector<std::map<
|
||||||
std::int64_t,
|
std::int64_t,
|
||||||
std::pair<EntityIndex, EntityIndex>>> partSectionAssignments_;
|
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> currentPart_;
|
||||||
std::optional<std::size_t> pendingSection_;
|
std::optional<std::size_t> pendingSection_;
|
||||||
std::optional<std::size_t> materialEligible_;
|
std::optional<std::size_t> materialEligible_;
|
||||||
|
std::optional<ElementFamily> modelElementFamily_;
|
||||||
bool headingSeen_{false};
|
bool headingSeen_{false};
|
||||||
bool partElementsSeen_{false};
|
bool partElementsSeen_{false};
|
||||||
bool partSetsSeen_{false};
|
bool partSetsSeen_{false};
|
||||||
|
|||||||
@@ -128,6 +128,55 @@ Tip, 2, -1.
|
|||||||
)inp";
|
)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) {
|
std::string supportedInventoryDeck(bool includeNoOps) {
|
||||||
const std::string preprint = includeNoOps
|
const std::string preprint = includeNoOps
|
||||||
? "*Preprint, echo=NO, model=NO, history=NO, contact=NO\n"
|
? "*Preprint, echo=NO, model=NO, history=NO, contact=NO\n"
|
||||||
@@ -422,6 +471,188 @@ TEST(InpDomainMapping, NoOpAllowlistWarnsWithoutSemanticEffect) {
|
|||||||
plain.value().steps()[0].loads.size());
|
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) {
|
TEST(InpDomainMapping, RejectsUnsupportedAndInvalidPortfolio) {
|
||||||
struct InvalidCase {
|
struct InvalidCase {
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|||||||
Reference in New Issue
Block a user