Files
FESADev/src/fesa/io/abaqus/domain_mapper.cpp
T

2165 lines
81 KiB
C++

#include "fesa/io/abaqus/domain_mapper.hpp"
#include <algorithm>
#include <array>
#include <cerrno>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace fesa {
namespace {
std::string uppercaseAscii(std::string value) {
std::transform(
value.begin(), value.end(), value.begin(), [](char character) {
if (character >= 'a' && character <= 'z') {
return static_cast<char>(character - 'a' + 'A');
}
return character;
});
return value;
}
bool equalName(const std::string& left, const std::string& right) {
return uppercaseAscii(left) == uppercaseAscii(right);
}
std::vector<std::string> withoutTrailingEmpty(
std::vector<std::string> fields) {
while (!fields.empty() && fields.back().empty()) {
fields.pop_back();
}
return fields;
}
struct RawNode {
std::int64_t label;
std::string labelText;
std::array<double, 3> coordinates;
SourceLocation location;
};
struct RawElement {
std::int64_t label;
std::string labelText;
std::array<std::int64_t, 2> nodeLabels;
SourceLocation location;
};
struct RawSet {
std::string name;
std::vector<std::int64_t> members;
SourceLocation location;
};
struct RawSection {
std::string elementSetName;
std::string materialName;
std::array<double, 5> properties;
std::array<double, 3> firstAxis;
std::vector<std::array<double, 2>> sectionPoints;
SourceLocation location;
};
struct RawPart {
std::string name;
SourceLocation location;
std::vector<RawNode> nodes;
std::vector<RawElement> elements;
std::vector<RawSet> nodeSets;
std::vector<RawSet> elementSets;
std::vector<RawSection> sections;
};
struct RawInstance {
std::string name;
std::string partName;
SourceLocation location;
};
struct RawAssemblySet {
bool isNodeSet;
std::string name;
std::string instanceName;
std::vector<std::int64_t> members;
SourceLocation location;
};
struct RawMaterial {
std::string name;
double youngsModulus{0.0};
double poissonRatio{0.0};
bool hasElastic{false};
SourceLocation location;
SourceLocation elasticLocation;
};
struct RawStep {
SourceLocation location;
bool hasStatic{false};
std::array<double, 4> staticValues{};
std::vector<BoundaryCondition> boundaries;
std::vector<NodalLoad> loads;
};
struct MappingFailure {
FailureCategory category;
Diagnostic diagnostic;
};
class MappingContext {
public:
explicit MappingContext(const ParsedInput& input) : input_{input} {}
Result<Domain> run() {
parseBlocks();
if (!failure_) {
finalizeModel();
}
if (failure_) {
return Result<Domain>::failure(Status::failure(
failure_->category, {std::move(failure_->diagnostic)}));
}
sortDiagnostics(definition_.warnings);
return Domain::create(std::move(definition_));
}
private:
const KeywordParameter* parameter(
const KeywordBlock& block,
std::string_view name) const {
const auto found = std::find_if(
block.parameters.begin(),
block.parameters.end(),
[name](const KeywordParameter& candidate) {
return candidate.name == name;
});
return found == block.parameters.end() ? nullptr : &*found;
}
bool fail(
FailureCategory category,
std::string code,
const SourceLocation& location,
std::string keyword,
std::string entityIdentity,
std::string message) {
if (!failure_) {
failure_ = MappingFailure{
category,
{Severity::error,
std::move(code),
location,
std::move(keyword),
std::move(entityIdentity),
std::move(message)}};
}
return false;
}
bool inputFailure(
std::string code,
const SourceLocation& location,
std::string keyword,
std::string entityIdentity,
std::string message) {
return fail(
FailureCategory::input,
std::move(code),
location,
std::move(keyword),
std::move(entityIdentity),
std::move(message));
}
bool modelFailure(
std::string code,
const SourceLocation& location,
std::string keyword,
std::string entityIdentity,
std::string message) {
return fail(
FailureCategory::model,
std::move(code),
location,
std::move(keyword),
std::move(entityIdentity),
std::move(message));
}
bool invalidKeywordLocation(
const KeywordBlock& block,
std::string message) {
return inputFailure(
"invalid-keyword-location",
block.location,
block.canonicalName,
"",
std::move(message));
}
bool validateParameters(
const KeywordBlock& block,
const std::vector<std::string_view>& allowed) {
std::set<std::string> seen;
for (const auto& candidate : block.parameters) {
if (!seen.insert(candidate.name).second) {
return inputFailure(
"duplicate-entity",
block.location,
block.canonicalName,
candidate.name,
"A keyword parameter may be declared only once.");
}
if (std::find(allowed.begin(), allowed.end(), candidate.name) ==
allowed.end()) {
return inputFailure(
"invalid-keyword-parameter",
block.location,
block.canonicalName,
candidate.name,
"The keyword parameter is outside the approved subset.");
}
}
return true;
}
const std::string* requiredParameterValue(
const KeywordBlock& block,
std::string_view name) {
const auto* found = parameter(block, name);
if (found == nullptr || !found->value || found->value->empty()) {
inputFailure(
"invalid-keyword-parameter",
block.location,
block.canonicalName,
std::string{name},
"The keyword requires a non-empty parameter value.");
return nullptr;
}
return &*found->value;
}
bool requireNoData(const KeywordBlock& block) {
if (!block.data.empty()) {
return inputFailure(
"invalid-data-arity",
block.data.front().location,
block.canonicalName,
"",
"This keyword does not accept data rows.");
}
return true;
}
bool parseInteger(
const std::string& text,
std::int64_t& value,
const SourceLocation& location,
const std::string& keyword,
bool positive) {
if (text.empty()) {
return inputFailure(
"invalid-numeric-value", location, keyword, text,
"A numeric field cannot be empty.");
}
char* end = nullptr;
errno = 0;
const long long parsed = std::strtoll(text.c_str(), &end, 10);
if (errno == ERANGE || end != text.c_str() + text.size() ||
(positive && parsed <= 0)) {
return inputFailure(
"invalid-numeric-value", location, keyword, text,
"The field must be a valid positive base-10 integer.");
}
value = static_cast<std::int64_t>(parsed);
return true;
}
bool tryPositiveInteger(const std::string& text, std::int64_t& value) const {
if (text.empty()) {
return false;
}
char* end = nullptr;
errno = 0;
const long long parsed = std::strtoll(text.c_str(), &end, 10);
if (errno == ERANGE || end != text.c_str() + text.size() || parsed <= 0) {
return false;
}
value = static_cast<std::int64_t>(parsed);
return true;
}
bool parseDouble(
const std::string& text,
double& value,
const SourceLocation& location,
const std::string& keyword) {
if (text.empty()) {
return inputFailure(
"invalid-numeric-value", location, keyword, text,
"A numeric field cannot be empty.");
}
char* end = nullptr;
errno = 0;
const double parsed = std::strtod(text.c_str(), &end);
if (errno == ERANGE || end != text.c_str() + text.size() ||
!std::isfinite(parsed)) {
return inputFailure(
"invalid-numeric-value", location, keyword, text,
"The field must be a finite floating-point value.");
}
value = parsed;
return true;
}
bool parseModelDouble(
const std::string& text,
double& value,
const SourceLocation& location,
const std::string& keyword,
const std::string& diagnosticCode,
const std::string& message) {
if (text.empty()) {
return inputFailure(
"invalid-numeric-value", location, keyword, text,
"A numeric field cannot be empty.");
}
char* end = nullptr;
errno = 0;
const double parsed = std::strtod(text.c_str(), &end);
if (end != text.c_str() + text.size()) {
return inputFailure(
"invalid-numeric-value", location, keyword, text,
"The field must use floating-point syntax.");
}
if (errno == ERANGE || !std::isfinite(parsed)) {
return modelFailure(
diagnosticCode, location, keyword, text, message);
}
value = parsed;
return true;
}
bool containsName(
const std::vector<RawPart>& values,
const std::string& name) const {
return std::any_of(
values.begin(), values.end(), [&name](const RawPart& value) {
return equalName(value.name, name);
});
}
bool containsName(
const std::vector<RawInstance>& values,
const std::string& name) const {
return std::any_of(
values.begin(), values.end(), [&name](const RawInstance& value) {
return equalName(value.name, name);
});
}
bool containsName(
const std::vector<RawMaterial>& values,
const std::string& name) const {
return std::any_of(
values.begin(), values.end(), [&name](const RawMaterial& value) {
return equalName(value.name, name);
});
}
bool containsSetName(const RawPart& part, const std::string& name) const {
const auto matches = [&name](const RawSet& set) {
return equalName(set.name, name);
};
return std::any_of(part.nodeSets.begin(), part.nodeSets.end(), matches) ||
std::any_of(part.elementSets.begin(), part.elementSets.end(), matches);
}
bool containsAssemblySetName(const std::string& name) const {
return std::any_of(
assemblySets_.begin(),
assemblySets_.end(),
[&name](const RawAssemblySet& set) {
return equalName(set.name, name);
});
}
void parseBlocks() {
definition_.sourcePath = input_.sourcePath;
definition_.sourceContentIdentity = input_.sourceContentIdentity;
for (std::size_t index = 0U;
index < input_.blocks.size() && !failure_;
++index) {
const auto& block = input_.blocks[index];
if (inInstance_) {
parseInstanceBlock(block);
} else if (currentPart_) {
parsePartBlock(block);
} else if (inAssembly_) {
parseAssemblyBlock(block);
} else if (inStep_) {
parseStepBlock(block);
} else {
parseTopLevelBlock(block, index);
}
}
if (!failure_ &&
(currentPart_ || inAssembly_ || inInstance_ || inStep_)) {
const auto location = input_.blocks.empty()
? SourceLocation{input_.sourcePath, 0U}
: input_.blocks.back().location;
inputFailure(
"unclosed-keyword-block",
location,
"",
"",
"A part, assembly, instance, or step block was not closed.");
}
}
void parseTopLevelBlock(const KeywordBlock& block, std::size_t index) {
if (stepSeen_) {
if (block.canonicalName == "STEP") {
parseStepStart(block);
} else {
invalidKeywordLocation(
block,
"The sole analysis step must be the final top-level block.");
}
return;
}
if (block.canonicalName != "ELASTIC") {
materialEligible_.reset();
}
pendingSection_.reset();
activeOutput_ = false;
if (block.canonicalName == "HEADING") {
if (index != 0U || headingSeen_ ||
!validateParameters(block, {})) {
if (!failure_) {
inputFailure(
"invalid-keyword-location",
block.location,
block.canonicalName,
"",
"HEADING is optional only as the first keyword.");
}
return;
}
headingSeen_ = true;
for (std::size_t row = 0U; row < block.data.size(); ++row) {
if (row != 0U) {
definition_.heading.push_back('\n');
}
for (std::size_t field = 0U;
field < block.data[row].fields.size();
++field) {
if (field != 0U) {
definition_.heading.push_back(',');
}
definition_.heading += block.data[row].fields[field];
}
}
return;
}
if (block.canonicalName == "PREPRINT") {
if (!requireNoData(block)) {
return;
}
addIgnoredWarning(block);
return;
}
if (block.canonicalName == "PART") {
parsePartStart(block);
return;
}
if (block.canonicalName == "ASSEMBLY") {
parseAssemblyStart(block);
return;
}
if (block.canonicalName == "MATERIAL") {
parseMaterial(block);
return;
}
if (block.canonicalName == "ELASTIC") {
parseElastic(block);
return;
}
if (block.canonicalName == "BOUNDARY") {
if (!assemblySeen_ || materials_.empty()) {
invalidKeywordLocation(
block,
"Model boundary data follows the assembly and materials.");
return;
}
modelBoundarySeen_ = true;
parseBoundary(block, modelBoundaries_);
return;
}
if (block.canonicalName == "STEP") {
parseStepStart(block);
return;
}
if (block.canonicalName == "END PART" ||
block.canonicalName == "END ASSEMBLY" ||
block.canonicalName == "END INSTANCE" ||
block.canonicalName == "END STEP") {
inputFailure(
"invalid-keyword-location",
block.location,
block.canonicalName,
"",
"The closing keyword has no matching open block.");
return;
}
rejectUnknown(block);
}
void parsePartStart(const KeywordBlock& block) {
if (assemblySeen_ || !materials_.empty() || modelBoundarySeen_) {
invalidKeywordLocation(
block,
"All part blocks must precede the sole assembly block.");
return;
}
if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) {
return;
}
const auto* name = requiredParameterValue(block, "NAME");
if (name == nullptr) {
return;
}
if (containsName(parts_, *name)) {
inputFailure(
"duplicate-entity",
block.location,
block.canonicalName,
*name,
"Part names are unique under case-insensitive lookup.");
return;
}
parts_.push_back({*name, block.location, {}, {}, {}, {}, {}});
currentPart_ = parts_.size() - 1U;
partElementsSeen_ = false;
partSetsSeen_ = false;
partSectionsSeen_ = false;
beamSectionContextActive_ = false;
}
void parsePartBlock(const KeywordBlock& block) {
if (block.canonicalName == "END PART") {
if (!validateParameters(block, {}) || !requireNoData(block)) {
return;
}
const RawPart& part = parts_[*currentPart_];
if (part.nodes.empty() || part.elements.empty() ||
part.sections.empty()) {
invalidKeywordLocation(
block,
"A part closes only after node, element, and beam-section blocks.");
return;
}
currentPart_.reset();
pendingSection_.reset();
beamSectionContextActive_ = false;
return;
}
if (block.canonicalName == "PART") {
inputFailure(
"invalid-keyword-location",
block.location,
block.canonicalName,
"",
"Nested part blocks are not supported.");
return;
}
if (block.canonicalName == "ASSEMBLY") {
inputFailure(
"unsupported-nested-assembly",
block.location,
block.canonicalName,
"",
"An assembly cannot be nested in a part.");
return;
}
RawPart& part = parts_[*currentPart_];
if (block.canonicalName == "NODE") {
beamSectionContextActive_ = false;
if (partElementsSeen_ || partSetsSeen_ || partSectionsSeen_) {
invalidKeywordLocation(
block,
"NODE blocks must precede element, set, and section blocks.");
return;
}
pendingSection_.reset();
parseNodes(block, part);
} else if (block.canonicalName == "ELEMENT") {
beamSectionContextActive_ = false;
if (part.nodes.empty() || partSetsSeen_ || partSectionsSeen_) {
invalidKeywordLocation(
block,
"ELEMENT blocks follow at least one NODE block and precede sets and sections.");
return;
}
pendingSection_.reset();
parseElements(block, part);
partElementsSeen_ = !failure_;
} else if (block.canonicalName == "NSET") {
beamSectionContextActive_ = false;
if (!partElementsSeen_ || partSectionsSeen_) {
invalidKeywordLocation(
block,
"Part sets follow element blocks and precede beam sections.");
return;
}
pendingSection_.reset();
parseSet(block, true, part);
partSetsSeen_ = !failure_;
} else if (block.canonicalName == "ELSET") {
beamSectionContextActive_ = false;
if (!partElementsSeen_ || partSectionsSeen_) {
invalidKeywordLocation(
block,
"Part sets follow element blocks and precede beam sections.");
return;
}
pendingSection_.reset();
parseSet(block, false, part);
partSetsSeen_ = !failure_;
} else if (block.canonicalName == "BEAM GENERAL SECTION") {
if (!partElementsSeen_) {
invalidKeywordLocation(
block,
"BEAM GENERAL SECTION follows the part mesh and optional sets.");
return;
}
parseBeamSection(block, part);
partSectionsSeen_ = !failure_;
beamSectionContextActive_ = !failure_;
} else if (block.canonicalName == "SECTION POINTS") {
parseSectionPoints(block, part);
} else if (block.canonicalName == "TRANSVERSE SHEAR STIFFNESS") {
pendingSection_.reset();
if (!beamSectionContextActive_) {
inputFailure(
"invalid-keyword-location",
block.location,
block.canonicalName,
"",
"The ignored shear keyword still requires beam-section context.");
return;
}
addIgnoredWarning(block);
} else {
pendingSection_.reset();
beamSectionContextActive_ = false;
rejectUnknown(block);
}
}
void parseNodes(const KeywordBlock& block, RawPart& part) {
if (!validateParameters(block, {}) || block.data.empty()) {
if (!failure_) {
inputFailure(
"invalid-data-arity", block.location, block.canonicalName,
"", "NODE requires at least one four-field data row.");
}
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != 4U) {
inputFailure(
"invalid-data-arity", row.location, block.canonicalName,
"", "NODE rows require label, x, y, z.");
return;
}
RawNode node{};
node.labelText = row.fields[0];
node.location = row.location;
if (!parseInteger(
row.fields[0], node.label, row.location,
block.canonicalName, true)) {
return;
}
if (std::any_of(
part.nodes.begin(), part.nodes.end(),
[&node](const RawNode& existing) {
return existing.label == node.label;
})) {
inputFailure(
"duplicate-entity", row.location, block.canonicalName,
node.labelText, "Node labels are unique within a part.");
return;
}
for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
if (!parseDouble(
row.fields[coordinate + 1U],
node.coordinates[coordinate],
row.location,
block.canonicalName)) {
return;
}
}
part.nodes.push_back(std::move(node));
}
}
void parseElements(const KeywordBlock& block, RawPart& part) {
if (!validateParameters(block, {"TYPE"})) {
return;
}
const auto* type = requiredParameterValue(block, "TYPE");
if (type == nullptr) {
return;
}
if (!equalName(*type, "B33")) {
inputFailure(
"unsupported-element-formulation",
block.location,
block.canonicalName,
*type,
"Only TYPE=B33 maps to the V0 Euler beam.");
return;
}
if (block.data.empty()) {
inputFailure(
"invalid-data-arity", block.location, block.canonicalName,
"", "ELEMENT requires at least one three-field row.");
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != 3U) {
inputFailure(
"invalid-data-arity", row.location, block.canonicalName,
"", "B33 rows require label, node 1, node 2.");
return;
}
RawElement element{};
element.labelText = row.fields[0];
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;
}
if (std::any_of(
part.elements.begin(), part.elements.end(),
[&element](const RawElement& existing) {
return existing.label == element.label;
})) {
inputFailure(
"duplicate-entity", row.location, block.canonicalName,
element.labelText,
"Element labels are unique within a part.");
return;
}
part.elements.push_back(std::move(element));
}
}
bool parseSetMembers(
const KeywordBlock& block,
bool generate,
std::vector<std::int64_t>& members) {
if (block.data.empty()) {
return inputFailure(
"invalid-data-arity", block.location, block.canonicalName,
"", "A set requires at least one member row.");
}
for (const auto& data : block.data) {
auto fields = withoutTrailingEmpty(data.fields);
if (generate) {
if (fields.size() != 3U) {
return inputFailure(
"invalid-data-arity", data.location,
block.canonicalName, "",
"GENERATE rows require first, last, increment.");
}
std::int64_t first = 0;
std::int64_t last = 0;
std::int64_t increment = 0;
if (!parseInteger(
fields[0], first, data.location,
block.canonicalName, true) ||
!parseInteger(
fields[1], last, data.location,
block.canonicalName, true)) {
return false;
}
if (!parseInteger(
fields[2], increment, data.location,
block.canonicalName, false) ||
increment <= 0 || first > last ||
(last - first) % increment != 0) {
return inputFailure(
"invalid-set-range", data.location,
block.canonicalName, "",
"GENERATE requires an inclusive range reached by a positive increment.");
}
for (std::int64_t label = first; label <= last;) {
members.push_back(label);
if (label > last - increment) {
break;
}
label += increment;
}
} else {
if (fields.empty() ||
std::any_of(
fields.begin(), fields.end(),
[](const std::string& field) { return field.empty(); })) {
return inputFailure(
"invalid-data-arity", data.location,
block.canonicalName, "",
"Explicit set rows require non-empty member labels.");
}
for (const auto& field : fields) {
std::int64_t label = 0;
if (!parseInteger(
field, label, data.location,
block.canonicalName, true)) {
return false;
}
members.push_back(label);
}
}
}
std::set<std::int64_t> unique;
for (const auto member : members) {
if (!unique.insert(member).second) {
return inputFailure(
"duplicate-entity", block.location,
block.canonicalName, std::to_string(member),
"A set cannot repeat the same source member.");
}
}
return true;
}
void parseSet(const KeywordBlock& block, bool nodeSet, RawPart& part) {
const std::string_view nameParameter = nodeSet ? "NSET" : "ELSET";
if (!validateParameters(block, {nameParameter, "GENERATE"})) {
return;
}
const auto* name = requiredParameterValue(block, nameParameter);
if (name == nullptr) {
return;
}
const auto* generateParameter = parameter(block, "GENERATE");
if (generateParameter != nullptr && generateParameter->value) {
inputFailure(
"invalid-keyword-parameter", block.location,
block.canonicalName, "GENERATE",
"GENERATE is a valueless flag.");
return;
}
if (containsSetName(part, *name)) {
inputFailure(
"duplicate-entity", block.location, block.canonicalName,
*name, "Set names are unique within a part.");
return;
}
RawSet set{*name, {}, block.location};
if (!parseSetMembers(block, generateParameter != nullptr, set.members)) {
return;
}
if (nodeSet) {
part.nodeSets.push_back(std::move(set));
} else {
part.elementSets.push_back(std::move(set));
}
}
void parseBeamSection(const KeywordBlock& block, RawPart& part) {
pendingSection_.reset();
if (!validateParameters(block, {"ELSET", "MATERIAL", "SECTION"})) {
return;
}
const auto* elementSet = requiredParameterValue(block, "ELSET");
const auto* material = requiredParameterValue(block, "MATERIAL");
const auto* section = requiredParameterValue(block, "SECTION");
if (elementSet == nullptr || material == nullptr || section == nullptr) {
return;
}
if (!equalName(*section, "GENERAL")) {
inputFailure(
"unsupported-section-formulation", block.location,
block.canonicalName, *section,
"Only SECTION=GENERAL belongs to the approved subset.");
return;
}
if (std::any_of(
part.sections.begin(), part.sections.end(),
[elementSet](const RawSection& existing) {
return equalName(existing.elementSetName, *elementSet);
})) {
inputFailure(
"duplicate-entity", block.location, block.canonicalName,
*elementSet,
"An element set can receive only one beam section.");
return;
}
if (block.data.size() != 2U || block.data[0].fields.size() != 5U ||
block.data[1].fields.size() != 3U) {
inputFailure(
"invalid-data-arity", block.location, block.canonicalName,
*elementSet,
"A general section requires one five-field property row and one three-field axis row.");
return;
}
RawSection raw{*elementSet, *material, {}, {}, {}, block.location};
for (std::size_t property = 0U; property < 5U; ++property) {
if (!parseModelDouble(
block.data[0].fields[property], raw.properties[property],
block.data[0].location, block.canonicalName,
"invalid-beam-property",
"Beam section properties must be finite.")) {
return;
}
}
for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
if (!parseModelDouble(
block.data[1].fields[coordinate], raw.firstAxis[coordinate],
block.data[1].location, block.canonicalName,
"invalid-beam-guide-vector",
"The beam guide vector must be finite.")) {
return;
}
}
part.sections.push_back(std::move(raw));
pendingSection_ = part.sections.size() - 1U;
}
void parseSectionPoints(const KeywordBlock& block, RawPart& part) {
if (!pendingSection_ || !validateParameters(block, {}) ||
block.data.empty()) {
if (!failure_) {
inputFailure(
"invalid-keyword-location", block.location,
block.canonicalName, "",
"SECTION POINTS must immediately follow a general section.");
}
return;
}
RawSection& section = part.sections[*pendingSection_];
for (const auto& row : block.data) {
if (row.fields.size() != 2U) {
inputFailure(
"invalid-data-arity", row.location,
block.canonicalName, section.elementSetName,
"Section-point rows require x1 and x2.");
return;
}
std::array<double, 2> point{};
if (!parseDouble(
row.fields[0], point[0], row.location,
block.canonicalName) ||
!parseDouble(
row.fields[1], point[1], row.location,
block.canonicalName)) {
return;
}
if (std::find(
section.sectionPoints.begin(),
section.sectionPoints.end(), point) !=
section.sectionPoints.end()) {
inputFailure(
"duplicate-entity", row.location,
block.canonicalName, section.elementSetName,
"Section points must be unique within a section.");
return;
}
section.sectionPoints.push_back(point);
}
pendingSection_.reset();
}
void parseAssemblyStart(const KeywordBlock& block) {
if (assemblySeen_) {
inputFailure(
"unsupported-nested-assembly", block.location,
block.canonicalName, "",
"V0 accepts exactly one non-nested assembly.");
return;
}
if (parts_.empty() || !materials_.empty() || modelBoundarySeen_) {
invalidKeywordLocation(
block,
"The sole assembly follows all part blocks and precedes model data.");
return;
}
if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) {
return;
}
if (requiredParameterValue(block, "NAME") == nullptr) {
return;
}
assemblySeen_ = true;
inAssembly_ = true;
assemblySetSeen_ = false;
}
void parseAssemblyBlock(const KeywordBlock& block) {
if (block.canonicalName == "END ASSEMBLY") {
if (!validateParameters(block, {}) || !requireNoData(block)) {
return;
}
if (instances_.empty()) {
invalidKeywordLocation(
block,
"The assembly requires at least one identity instance.");
return;
}
inAssembly_ = false;
return;
}
if (block.canonicalName == "ASSEMBLY") {
inputFailure(
"unsupported-nested-assembly", block.location,
block.canonicalName, "",
"Nested or duplicate assembly blocks are unsupported.");
} else if (block.canonicalName == "INSTANCE") {
if (assemblySetSeen_) {
invalidKeywordLocation(
block,
"All identity instances must precede assembly-level sets.");
return;
}
parseInstanceStart(block);
} else if (block.canonicalName == "NSET") {
if (instances_.empty()) {
invalidKeywordLocation(
block,
"Assembly sets follow at least one identity instance.");
return;
}
assemblySetSeen_ = true;
parseAssemblySet(block, true);
} else if (block.canonicalName == "ELSET") {
if (instances_.empty()) {
invalidKeywordLocation(
block,
"Assembly sets follow at least one identity instance.");
return;
}
assemblySetSeen_ = true;
parseAssemblySet(block, false);
} else {
rejectUnknown(block);
}
}
void parseInstanceStart(const KeywordBlock& block) {
if (parameter(block, "DEPENDENT") != nullptr ||
parameter(block, "INDEPENDENT") != nullptr) {
inputFailure(
"unsupported-instance-mesh-semantics", block.location,
block.canonicalName, "",
"Dependent and independent instance mesh semantics are unsupported.");
return;
}
if (!validateParameters(block, {"NAME", "PART"})) {
return;
}
const auto* name = requiredParameterValue(block, "NAME");
const auto* part = requiredParameterValue(block, "PART");
if (name == nullptr || part == nullptr) {
return;
}
if (!block.data.empty()) {
inputFailure(
"unsupported-instance-transform", block.data.front().location,
block.canonicalName, *name,
"Instance translation or rotation data is unsupported.");
return;
}
if (containsName(instances_, *name)) {
inputFailure(
"duplicate-entity", block.location, block.canonicalName,
*name, "Instance names are globally unique.");
return;
}
instances_.push_back({*name, *part, block.location});
inInstance_ = true;
}
void parseInstanceBlock(const KeywordBlock& block) {
if (block.canonicalName == "END INSTANCE") {
if (!validateParameters(block, {}) || !requireNoData(block)) {
return;
}
inInstance_ = false;
return;
}
if (block.canonicalName == "ASSEMBLY") {
inputFailure(
"unsupported-nested-assembly", block.location,
block.canonicalName, "",
"An assembly cannot be nested in an instance.");
return;
}
inputFailure(
"unsupported-instance-mesh-semantics", block.location,
block.canonicalName, instances_.back().name,
"Instance-local mesh definitions are unsupported.");
}
void parseAssemblySet(const KeywordBlock& block, bool nodeSet) {
const std::string_view nameParameter = nodeSet ? "NSET" : "ELSET";
if (!validateParameters(
block, {nameParameter, "INSTANCE", "GENERATE"})) {
return;
}
const auto* name = requiredParameterValue(block, nameParameter);
const auto* instance = requiredParameterValue(block, "INSTANCE");
if (name == nullptr || instance == nullptr) {
return;
}
const auto* generateParameter = parameter(block, "GENERATE");
if (generateParameter != nullptr && generateParameter->value) {
inputFailure(
"invalid-keyword-parameter", block.location,
block.canonicalName, "GENERATE",
"GENERATE is a valueless flag.");
return;
}
if (containsAssemblySetName(*name)) {
inputFailure(
"duplicate-entity", block.location, block.canonicalName,
*name, "Assembly set names are unique.");
return;
}
RawAssemblySet set{nodeSet, *name, *instance, {}, block.location};
if (!parseSetMembers(block, generateParameter != nullptr, set.members)) {
return;
}
assemblySets_.push_back(std::move(set));
}
void parseMaterial(const KeywordBlock& block) {
if (!assemblySeen_ || modelBoundarySeen_) {
invalidKeywordLocation(
block,
"Material definitions follow the assembly and precede model boundaries.");
return;
}
if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) {
return;
}
const auto* name = requiredParameterValue(block, "NAME");
if (name == nullptr) {
return;
}
if (containsName(materials_, *name)) {
inputFailure(
"duplicate-entity", block.location, block.canonicalName,
*name, "Material names are globally unique.");
return;
}
materials_.push_back({*name, 0.0, 0.0, false, block.location, {}});
materialEligible_ = materials_.size() - 1U;
}
void parseElastic(const KeywordBlock& block) {
if (!materialEligible_) {
inputFailure(
"invalid-keyword-location", block.location,
block.canonicalName, "",
"ELASTIC must immediately follow MATERIAL.");
return;
}
if (!validateParameters(block, {}) || block.data.size() != 1U ||
block.data[0].fields.size() != 2U) {
if (!failure_) {
inputFailure(
"invalid-data-arity", block.location,
block.canonicalName, materials_[*materialEligible_].name,
"ELASTIC requires exactly one E, nu row.");
}
return;
}
RawMaterial& material = materials_[*materialEligible_];
if (material.hasElastic) {
inputFailure(
"duplicate-entity", block.location, block.canonicalName,
material.name, "A material accepts one ELASTIC definition.");
return;
}
if (!parseModelDouble(
block.data[0].fields[0], material.youngsModulus,
block.data[0].location, block.canonicalName,
"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",
"Elastic material values must be finite.")) {
return;
}
material.hasElastic = true;
material.elasticLocation = block.data[0].location;
}
void parseBoundary(
const KeywordBlock& block,
std::vector<BoundaryCondition>& destination) {
if (!validateParameters(block, {}) || block.data.empty()) {
if (!failure_) {
inputFailure(
"invalid-data-arity", block.location,
block.canonicalName, "",
"BOUNDARY requires one or more data rows.");
}
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != 3U && row.fields.size() != 4U) {
inputFailure(
"invalid-data-arity", row.location,
block.canonicalName, "",
"BOUNDARY rows require target, first DOF, last DOF, and optional value.");
return;
}
if (row.fields[0].empty()) {
inputFailure(
"unresolved-reference", row.location,
block.canonicalName, "",
"A boundary target cannot be empty.");
return;
}
std::int64_t first = 0;
std::int64_t last = 0;
if (!parseInteger(
row.fields[1], first, row.location,
block.canonicalName, true) ||
!parseInteger(
row.fields[2], last, row.location,
block.canonicalName, true)) {
return;
}
if (first < 1 || first > 6 || last < first || last > 6) {
inputFailure(
"invalid-dof", row.location, block.canonicalName,
row.fields[0], "Boundary DOFs must be an ordered range in 1..6.");
return;
}
double value = 0.0;
if (row.fields.size() == 4U &&
!parseDouble(
row.fields[3], value, row.location,
block.canonicalName)) {
return;
}
destination.push_back({
row.fields[0],
static_cast<int>(first),
static_cast<int>(last),
value,
row.location});
}
}
void parseCload(const KeywordBlock& block, std::vector<NodalLoad>& loads) {
if (!validateParameters(block, {}) || block.data.empty()) {
if (!failure_) {
inputFailure(
"invalid-data-arity", block.location,
block.canonicalName, "", "CLOAD requires one or more rows.");
}
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != 3U || row.fields[0].empty()) {
inputFailure(
"invalid-data-arity", row.location,
block.canonicalName, "",
"CLOAD rows require target, DOF, magnitude.");
return;
}
std::int64_t dof = 0;
double magnitude = 0.0;
if (!parseInteger(
row.fields[1], dof, row.location,
block.canonicalName, true) ||
!parseDouble(
row.fields[2], magnitude, row.location,
block.canonicalName)) {
return;
}
if (dof < 1 || dof > 6) {
inputFailure(
"invalid-dof", row.location, block.canonicalName,
row.fields[0], "CLOAD DOF must be in 1..6.");
return;
}
loads.push_back({
row.fields[0], static_cast<int>(dof), magnitude, row.location});
}
}
void parseStepStart(const KeywordBlock& block) {
if (stepSeen_) {
inputFailure(
"unsupported-multiple-step", block.location,
block.canonicalName, "",
"V0 accepts exactly one analysis step.");
return;
}
if (!assemblySeen_ || materials_.empty()) {
invalidKeywordLocation(
block,
"The sole step follows the complete assembly and material definitions.");
return;
}
if (!validateParameters(block, {"NAME", "NLGEOM"}) ||
!requireNoData(block)) {
return;
}
const auto* nlgeom = parameter(block, "NLGEOM");
if (nlgeom != nullptr) {
if (!nlgeom->value || nlgeom->value->empty()) {
inputFailure(
"invalid-keyword-parameter", block.location,
block.canonicalName, "NLGEOM",
"NLGEOM requires NO in the approved subset.");
return;
}
if (!equalName(*nlgeom->value, "NO")) {
modelFailure(
"unsupported-nonlinear-geometry", block.location,
block.canonicalName, *nlgeom->value,
"Only absent NLGEOM or NLGEOM=NO is supported.");
return;
}
}
stepSeen_ = true;
inStep_ = true;
step_ = RawStep{block.location, false, {}, {}, {}};
stepLoadSeen_ = false;
stepNoOpSeen_ = false;
}
void parseStepBlock(const KeywordBlock& block) {
if (block.canonicalName == "END STEP") {
activeOutput_ = false;
if (!validateParameters(block, {}) || !requireNoData(block)) {
return;
}
if (!step_.hasStatic) {
invalidKeywordLocation(
block,
"The sole step requires exactly one leading STATIC procedure.");
return;
}
inStep_ = false;
return;
}
if (block.canonicalName == "STEP") {
inputFailure(
"unsupported-multiple-step", block.location,
block.canonicalName, "",
"A second or nested step is unsupported.");
return;
}
if (block.canonicalName == "STATIC") {
activeOutput_ = false;
if (!step_.boundaries.empty() || !step_.loads.empty() ||
stepNoOpSeen_) {
invalidKeywordLocation(
block,
"STATIC must be the first keyword in the sole step.");
return;
}
parseStatic(block);
} else if (block.canonicalName == "BOUNDARY") {
activeOutput_ = false;
if (!step_.hasStatic || stepLoadSeen_ || stepNoOpSeen_) {
invalidKeywordLocation(
block,
"Step boundaries follow STATIC and precede loads and no-op requests.");
return;
}
parseBoundary(block, step_.boundaries);
} else if (block.canonicalName == "CLOAD") {
activeOutput_ = false;
if (!step_.hasStatic || stepNoOpSeen_) {
invalidKeywordLocation(
block,
"CLOAD follows STATIC and boundaries and precedes no-op requests.");
return;
}
parseCload(block, step_.loads);
stepLoadSeen_ = !failure_;
} else if (block.canonicalName == "RESTART") {
activeOutput_ = false;
if (!step_.hasStatic) {
invalidKeywordLocation(
block,
"Step no-op requests follow STATIC, boundaries, and loads.");
return;
}
stepNoOpSeen_ = true;
if (requireNoData(block)) {
addIgnoredWarning(block);
}
} else if (block.canonicalName == "OUTPUT") {
if (!step_.hasStatic) {
invalidKeywordLocation(
block,
"Step no-op requests follow STATIC, boundaries, and loads.");
return;
}
stepNoOpSeen_ = true;
parseOutputRoot(block);
} else if (block.canonicalName == "NODE OUTPUT" ||
block.canonicalName == "ELEMENT OUTPUT" ||
block.canonicalName == "CONTACT OUTPUT") {
parseOutputChild(block);
} else {
activeOutput_ = false;
rejectUnknown(block);
}
}
void parseStatic(const KeywordBlock& block) {
if (step_.hasStatic || !validateParameters(block, {}) ||
block.data.size() != 1U || block.data[0].fields.size() != 4U) {
if (!failure_) {
inputFailure(
"invalid-static-data", block.location,
block.canonicalName, "",
"STATIC requires exactly one row of four values.");
}
return;
}
for (std::size_t field = 0U; field < 4U; ++field) {
if (!parseDouble(
block.data[0].fields[field], step_.staticValues[field],
block.data[0].location, block.canonicalName)) {
return;
}
if (step_.staticValues[field] <= 0.0) {
inputFailure(
"invalid-static-data", block.data[0].location,
block.canonicalName, "",
"All four STATIC fields must be positive.");
return;
}
}
if (step_.staticValues[2] > step_.staticValues[3]) {
inputFailure(
"invalid-static-data", block.data[0].location,
block.canonicalName, "",
"STATIC minimum increment cannot exceed maximum increment.");
return;
}
step_.hasStatic = true;
}
void parseOutputRoot(const KeywordBlock& block) {
const auto* field = parameter(block, "FIELD");
const auto* history = parameter(block, "HISTORY");
if ((field == nullptr) == (history == nullptr) ||
(field != nullptr && field->value) ||
(history != nullptr && history->value)) {
inputFailure(
"unsupported-keyword", block.location,
block.canonicalName, "",
"OUTPUT must select exactly FIELD or HISTORY.");
return;
}
activeOutput_ = true;
addIgnoredWarning(block);
}
void parseOutputChild(const KeywordBlock& block) {
if (!activeOutput_) {
inputFailure(
"invalid-keyword-location", block.location,
block.canonicalName, "",
"Output variable keywords require an active OUTPUT request.");
return;
}
addIgnoredWarning(block);
}
void addIgnoredWarning(const KeywordBlock& block) {
// One warning per allowlisted keyword keeps no-op provenance stable;
// subordinate variable rows remain attached to that keyword record.
definition_.warnings.push_back({
Severity::warning,
"ignored-input-keyword",
block.location,
block.canonicalName,
"",
"The allowlisted Abaqus keyword is ignored without semantic effect."});
}
void rejectUnknown(const KeywordBlock& block) {
inputFailure(
"unsupported-keyword", block.location,
block.canonicalName, "",
"The keyword is outside the approved Abaqus subset.");
}
const RawPart* findPart(const std::string& name) const {
const auto found = std::find_if(
parts_.begin(), parts_.end(), [&name](const RawPart& part) {
return equalName(part.name, name);
});
return found == parts_.end() ? nullptr : &*found;
}
const RawInstance* findInstance(const std::string& name) const {
const auto found = std::find_if(
instances_.begin(), instances_.end(),
[&name](const RawInstance& instance) {
return equalName(instance.name, name);
});
return found == instances_.end() ? nullptr : &*found;
}
std::optional<EntityIndex> findMaterialIndex(const std::string& name) const {
for (std::size_t index = 0U; index < materials_.size(); ++index) {
if (equalName(materials_[index].name, name)) {
return static_cast<EntityIndex>(index);
}
}
return std::nullopt;
}
const RawSet* findSet(
const std::vector<RawSet>& sets,
const std::string& name) const {
const auto found = std::find_if(
sets.begin(), sets.end(), [&name](const RawSet& set) {
return equalName(set.name, name);
});
return found == sets.end() ? nullptr : &*found;
}
const RawNode* findNode(const RawPart& part, std::int64_t label) const {
const auto found = std::find_if(
part.nodes.begin(), part.nodes.end(),
[label](const RawNode& node) { return node.label == label; });
return found == part.nodes.end() ? nullptr : &*found;
}
const RawElement* findElement(
const RawPart& part,
std::int64_t label) const {
const auto found = std::find_if(
part.elements.begin(), part.elements.end(),
[label](const RawElement& element) { return element.label == label; });
return found == part.elements.end() ? nullptr : &*found;
}
void finalizeModel() {
if (parts_.empty() || !assemblySeen_ || instances_.empty() ||
materials_.empty() || !stepSeen_ || !step_.hasStatic) {
const auto location = input_.blocks.empty()
? SourceLocation{input_.sourcePath, 0U}
: input_.blocks.back().location;
inputFailure(
"invalid-model-cardinality", location, "", "",
"The model requires part, assembly, identity instance, material, and one STATIC step.");
return;
}
finalizeMaterials();
if (failure_) {
return;
}
finalizePartsAndSections();
if (failure_) {
return;
}
expandInstances();
if (failure_) {
return;
}
expandAssemblySets();
if (failure_) {
return;
}
finalizeStep();
}
void finalizeMaterials() {
for (const auto& material : materials_) {
if (!material.hasElastic) {
inputFailure(
"unresolved-reference", material.location,
"MATERIAL", material.name,
"A material must own exactly one ELASTIC row.");
return;
}
const double denominator = 2.0 * (1.0 + material.poissonRatio);
const double shearModulus = material.youngsModulus / denominator;
// The approved ledger deliberately validates derived G, not an
// independent upper bound on Poisson's ratio.
if (!(material.youngsModulus > 0.0) ||
!std::isfinite(shearModulus) || !(shearModulus > 0.0)) {
modelFailure(
"invalid-beam-property", material.elasticLocation,
"ELASTIC", material.name,
"E and the derived G=E/(2*(1+nu)) must be positive.");
return;
}
definition_.materials.push_back({
material.name,
material.youngsModulus,
material.poissonRatio,
material.location});
}
}
void finalizePartsAndSections() {
partSectionAssignments_.resize(parts_.size());
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()) {
inputFailure(
"invalid-model-cardinality", part.location,
"PART", part.name,
"A part requires nodes, B33 elements, and a beam section.");
return;
}
PartDefinition partDefinition{};
partDefinition.name = part.name;
partDefinition.location = part.location;
for (const auto& node : part.nodes) {
partDefinition.nodeSourceLabels.push_back(node.label);
}
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& set : part.nodeSets) {
partDefinition.nodeSetNames.push_back(set.name);
for (const auto label : set.members) {
if (findNode(part, label) == nullptr) {
inputFailure(
"unresolved-reference", set.location,
"NSET", set.name,
"Every node-set member must resolve within its part.");
return;
}
}
}
for (const auto& set : part.elementSets) {
partDefinition.elementSetNames.push_back(set.name);
for (const auto label : set.members) {
if (findElement(part, label) == nullptr) {
inputFailure(
"unresolved-reference", set.location,
"ELSET", set.name,
"Every element-set member must resolve within its part.");
return;
}
}
}
definition_.parts.push_back(std::move(partDefinition));
auto& assignments = partSectionAssignments_[partIndex];
for (const auto& section : part.sections) {
const auto* elementSet = findSet(
part.elementSets, section.elementSetName);
const auto materialIndex = findMaterialIndex(section.materialName);
if (elementSet == nullptr || !materialIndex) {
inputFailure(
"unresolved-reference", section.location,
"BEAM GENERAL SECTION", section.elementSetName,
"Section ELSET and MATERIAL references must resolve.");
return;
}
if (section.properties[2] != 0.0) {
modelFailure(
"unsupported-coupled-section", section.location,
"BEAM GENERAL SECTION", section.elementSetName,
"V0 requires exact I12=0.");
return;
}
if (!(section.properties[0] > 0.0) ||
!(section.properties[1] > 0.0) ||
!(section.properties[3] > 0.0) ||
!(section.properties[4] > 0.0)) {
modelFailure(
"invalid-beam-property", section.location,
"BEAM GENERAL SECTION", section.elementSetName,
"A, I11, I22, and J must be positive.");
return;
}
const EntityIndex sectionIndex =
static_cast<EntityIndex>(definition_.sections.size());
definition_.sections.push_back({
section.elementSetName,
section.properties[0],
section.properties[1],
section.properties[2],
section.properties[3],
section.properties[4],
section.firstAxis,
section.sectionPoints,
section.location});
for (const auto elementLabel : elementSet->members) {
if (!assignments.emplace(
elementLabel,
std::make_pair(sectionIndex, *materialIndex)).second) {
inputFailure(
"duplicate-entity", section.location,
"BEAM GENERAL SECTION",
std::to_string(elementLabel),
"An element cannot receive multiple section assignments.");
return;
}
}
}
for (const auto& element : part.elements) {
if (assignments.find(element.label) == assignments.end()) {
inputFailure(
"unresolved-reference", element.location,
"ELEMENT", element.labelText,
"Every B33 element requires a resolved section assignment.");
return;
}
}
}
}
std::size_t partIndex(const RawPart& part) const {
return static_cast<std::size_t>(&part - parts_.data());
}
bool validateGeometry(
const RawElement& raw,
const Node& first,
const Node& second,
const GeneralBeamSection& section) {
const auto norm = [](const std::array<double, 3>& vector) {
return std::hypot(vector[0], vector[1], vector[2]);
};
const auto maximumAbsolute = [](const std::array<double, 3>& vector) {
return std::max({
std::abs(vector[0]),
std::abs(vector[1]),
std::abs(vector[2])});
};
// Compare both approved inequalities after a common scaling. This
// preserves the exact ratios while avoiding overflow in x*x and in
// subtraction between large finite coordinates.
const double globalCoordinateScale = std::max({
1.0,
maximumAbsolute(first.coordinates),
maximumAbsolute(second.coordinates)});
std::array<double, 3> firstScaled{};
std::array<double, 3> secondScaled{};
std::array<double, 3> deltaScaled{};
for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
firstScaled[coordinate] =
first.coordinates[coordinate] / globalCoordinateScale;
secondScaled[coordinate] =
second.coordinates[coordinate] / globalCoordinateScale;
deltaScaled[coordinate] =
secondScaled[coordinate] - firstScaled[coordinate];
}
const double lengthRatio = norm(deltaScaled);
const double coordinateNormRatio = std::max({
1.0 / globalCoordinateScale,
norm(firstScaled),
norm(secondScaled)});
if (!(lengthRatio > 1.0e-12 * coordinateNormRatio)) {
return modelFailure(
"invalid-beam-length", raw.location, "ELEMENT",
raw.labelText,
"Beam length fails the approved scale-aware threshold.");
}
std::array<double, 3> tangent{
deltaScaled[0] / lengthRatio,
deltaScaled[1] / lengthRatio,
deltaScaled[2] / lengthRatio};
const double globalGuideScale =
std::max(1.0, maximumAbsolute(section.firstAxis));
std::array<double, 3> guideScaled{};
for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
guideScaled[coordinate] =
section.firstAxis[coordinate] / globalGuideScale;
}
const double projection =
guideScaled[0] * tangent[0] +
guideScaled[1] * tangent[1] +
guideScaled[2] * tangent[2];
std::array<double, 3> perpendicular{
guideScaled[0] - projection * tangent[0],
guideScaled[1] - projection * tangent[1],
guideScaled[2] - projection * tangent[2]};
const double guideNormRatio = std::max(
1.0 / globalGuideScale,
norm(guideScaled));
if (!(norm(perpendicular) > 1.0e-12 * guideNormRatio)) {
return modelFailure(
"invalid-beam-guide-vector", section.location,
"BEAM GENERAL SECTION", raw.labelText,
"The first section axis cannot be zero or tangent-parallel.");
}
return true;
}
void expandInstances() {
for (const auto& rawInstance : instances_) {
const RawPart* part = findPart(rawInstance.partName);
if (part == nullptr) {
inputFailure(
"unresolved-reference", rawInstance.location,
"INSTANCE", rawInstance.name,
"INSTANCE PART must resolve case-insensitively.");
return;
}
InstanceDefinition instance{
rawInstance.name, part->name, {}, {}, rawInstance.location};
std::map<std::int64_t, EntityIndex> nodeIndices;
std::map<std::int64_t, EntityIndex> elementIndices;
// Instance order, followed by part-local declaration order, is the
// sole source of stable expanded internal IDs.
for (const auto& rawNode : part->nodes) {
if (definition_.nodes.size() >
std::numeric_limits<EntityIndex>::max()) {
inputFailure(
"entity-index-overflow", rawNode.location,
"NODE", rawNode.labelText,
"Expanded node count exceeds EntityIndex capacity.");
return;
}
const EntityIndex index =
static_cast<EntityIndex>(definition_.nodes.size());
definition_.nodes.push_back({
{rawInstance.name, rawNode.label, rawNode.labelText},
rawNode.coordinates,
rawNode.location});
nodeIndices.emplace(rawNode.label, index);
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;
}
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});
}
for (const auto& rawSet : part->nodeSets) {
NodeSet set{rawSet.name, rawInstance.name, {}, rawSet.location};
for (const auto member : rawSet.members) {
const auto found = nodeIndices.find(member);
if (found == nodeIndices.end()) {
inputFailure(
"unresolved-reference", rawSet.location,
"NSET", rawSet.name,
"Part node-set expansion failed.");
return;
}
set.nodeIndices.push_back(found->second);
}
definition_.nodeSets.push_back(std::move(set));
}
for (const auto& rawSet : part->elementSets) {
ElementSet set{
rawSet.name, rawInstance.name, {}, rawSet.location};
for (const auto member : rawSet.members) {
const auto found = elementIndices.find(member);
if (found == elementIndices.end()) {
inputFailure(
"unresolved-reference", rawSet.location,
"ELSET", rawSet.name,
"Part element-set expansion failed.");
return;
}
set.elementIndices.push_back(found->second);
}
definition_.elementSets.push_back(std::move(set));
}
definition_.instances.push_back(std::move(instance));
}
}
void expandAssemblySets() {
for (const auto& rawSet : assemblySets_) {
const RawInstance* rawInstance = findInstance(rawSet.instanceName);
if (rawInstance == nullptr) {
inputFailure(
"unresolved-reference", rawSet.location,
rawSet.isNodeSet ? "NSET" : "ELSET", rawSet.name,
"Assembly set INSTANCE must resolve.");
return;
}
const auto definitionInstance = std::find_if(
definition_.instances.begin(), definition_.instances.end(),
[&rawInstance](const InstanceDefinition& instance) {
return equalName(instance.name, rawInstance->name);
});
if (definitionInstance == definition_.instances.end()) {
inputFailure(
"unresolved-reference", rawSet.location,
rawSet.isNodeSet ? "NSET" : "ELSET", rawSet.name,
"Assembly set instance expansion is unavailable.");
return;
}
if (rawSet.isNodeSet) {
NodeSet set{
rawSet.name, definitionInstance->name, {}, rawSet.location};
for (const auto member : rawSet.members) {
const auto mapping = std::find_if(
definitionInstance->nodeMappings.begin(),
definitionInstance->nodeMappings.end(),
[member](const SourceIndexMapping& value) {
return value.sourceLabel == member;
});
if (mapping == definitionInstance->nodeMappings.end()) {
inputFailure(
"unresolved-reference", rawSet.location,
"NSET", rawSet.name,
"Assembly node-set member must resolve in its instance.");
return;
}
set.nodeIndices.push_back(mapping->internalIndex);
}
definition_.nodeSets.push_back(std::move(set));
} else {
ElementSet set{
rawSet.name, definitionInstance->name, {}, rawSet.location};
for (const auto member : rawSet.members) {
const auto mapping = std::find_if(
definitionInstance->elementMappings.begin(),
definitionInstance->elementMappings.end(),
[member](const SourceIndexMapping& value) {
return value.sourceLabel == member;
});
if (mapping == definitionInstance->elementMappings.end()) {
inputFailure(
"unresolved-reference", rawSet.location,
"ELSET", rawSet.name,
"Assembly element-set member must resolve in its instance.");
return;
}
set.elementIndices.push_back(mapping->internalIndex);
}
definition_.elementSets.push_back(std::move(set));
}
}
}
std::optional<std::vector<EntityIndex>> resolveNodeTarget(
const std::string& target,
const SourceLocation& location,
const std::string& keyword) {
std::vector<const NodeSet*> matchingSets;
for (const auto& set : definition_.nodeSets) {
if (equalName(set.name, target)) {
matchingSets.push_back(&set);
}
}
std::vector<EntityIndex> matchingNodes;
std::int64_t label = 0;
if (tryPositiveInteger(target, label)) {
for (std::size_t index = 0U;
index < definition_.nodes.size();
++index) {
if (definition_.nodes[index].sourceId.sourceLabel == label) {
matchingNodes.push_back(static_cast<EntityIndex>(index));
}
}
}
// A token is resolved only after both approved interpretations have
// been considered; declaration order never gives a node set priority
// over an equally valid direct source-label target.
if (matchingSets.size() > 1U) {
inputFailure(
"unresolved-reference", location, keyword, target,
"The node-set target is ambiguous across identity instances.");
return std::nullopt;
}
if (matchingNodes.size() > 1U) {
inputFailure(
"unresolved-reference", location, keyword, target,
"The direct node-label target is ambiguous across identity instances.");
return std::nullopt;
}
if (matchingSets.size() == 1U && matchingNodes.size() == 1U) {
inputFailure(
"unresolved-reference", location, keyword, target,
"The target is ambiguous between a node-set name and a direct node label.");
return std::nullopt;
}
if (matchingSets.size() == 1U) {
return matchingSets.front()->nodeIndices;
}
if (matchingNodes.size() == 1U) {
return matchingNodes;
}
inputFailure(
"unresolved-reference", location, keyword, target,
"The boundary or load target must resolve to one node or node set.");
return std::nullopt;
}
void finalizeStep() {
std::vector<BoundaryCondition> boundaries = modelBoundaries_;
boundaries.insert(
boundaries.end(), step_.boundaries.begin(), step_.boundaries.end());
std::map<std::pair<EntityIndex, int>, double> prescribedValues;
for (const auto& boundary : boundaries) {
auto target = resolveNodeTarget(
boundary.target, boundary.location, "BOUNDARY");
if (!target) {
return;
}
for (const auto node : *target) {
for (int dof = boundary.firstDof; dof <= boundary.lastDof; ++dof) {
const auto key = std::make_pair(node, dof);
const auto existing = prescribedValues.find(key);
if (existing != prescribedValues.end() &&
existing->second != boundary.value) {
inputFailure(
"conflicting-boundary-condition",
boundary.location,
"BOUNDARY",
boundary.target,
"Expanded boundary rows prescribe different values to one node/DOF.");
return;
}
prescribedValues[key] = boundary.value;
}
}
}
for (const auto& load : step_.loads) {
if (!resolveNodeTarget(load.target, load.location, "CLOAD")) {
return;
}
}
// Static time-control values are provenance only: V0 creates exactly
// the canonical final frame 0 and performs no increment loop here.
definition_.steps.push_back({
"Step-1",
std::move(boundaries),
step_.loads,
step_.staticValues[0],
step_.staticValues[1],
step_.staticValues[2],
step_.staticValues[3],
step_.location});
}
const ParsedInput& input_;
ModelDefinition definition_{};
std::optional<MappingFailure> failure_;
std::vector<RawPart> parts_;
std::vector<RawInstance> instances_;
std::vector<RawAssemblySet> assemblySets_;
std::vector<RawMaterial> materials_;
std::vector<BoundaryCondition> modelBoundaries_;
RawStep step_{};
std::vector<std::map<
std::int64_t,
std::pair<EntityIndex, EntityIndex>>> partSectionAssignments_;
std::optional<std::size_t> currentPart_;
std::optional<std::size_t> pendingSection_;
std::optional<std::size_t> materialEligible_;
bool headingSeen_{false};
bool partElementsSeen_{false};
bool partSetsSeen_{false};
bool partSectionsSeen_{false};
bool beamSectionContextActive_{false};
bool assemblySeen_{false};
bool assemblySetSeen_{false};
bool modelBoundarySeen_{false};
bool inAssembly_{false};
bool inInstance_{false};
bool stepSeen_{false};
bool inStep_{false};
bool stepLoadSeen_{false};
bool stepNoOpSeen_{false};
bool activeOutput_{false};
};
} // namespace
Result<Domain> AbaqusDomainMapper::map(const ParsedInput& input) const {
return MappingContext{input}.run();
}
} // namespace fesa