#include "fesa/io/abaqus/domain_mapper.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include 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(character - 'a' + 'A'); } return character; }); return value; } bool equalName(const std::string& left, const std::string& right) { return uppercaseAscii(left) == uppercaseAscii(right); } std::vector withoutTrailingEmpty( std::vector fields) { while (!fields.empty() && fields.back().empty()) { fields.pop_back(); } return fields; } struct RawNode { std::int64_t label; std::string labelText; std::array coordinates; SourceLocation location; }; struct RawElement { enum class Type { b33, s4, s4r }; std::int64_t label; std::string labelText; Type type; std::vector nodeLabels; SourceLocation location; }; enum class ElementFamily { beam, shell }; struct RawSet { std::string name; std::vector members; SourceLocation location; }; struct RawSection { std::string elementSetName; std::string materialName; std::array properties; std::array firstAxis; std::vector> sectionPoints; SourceLocation location; }; struct RawShellSection { std::string elementSetName; std::string materialName; double thickness; SourceLocation location; }; struct RawPart { std::string name; SourceLocation location; std::vector nodes; std::vector elements; std::vector nodeSets; std::vector elementSets; std::vector sections; std::vector shellSections; }; struct RawInstance { std::string name; std::string partName; SourceLocation location; }; struct RawAssemblySet { bool isNodeSet; std::string name; std::string instanceName; std::vector 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 staticValues{}; std::vector boundaries; std::vector loads; }; struct MappingFailure { FailureCategory category; Diagnostic diagnostic; }; class MappingContext { public: explicit MappingContext(const ParsedInput& input) : input_{input} {} Result run() { parseBlocks(); if (!failure_) { finalizeModel(); } if (failure_) { return Result::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& allowed) { std::set 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(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(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& 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& 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& 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_]; const bool shellPart = modelElementFamily_ == ElementFamily::shell; if (part.nodes.empty() || part.elements.empty() || (!shellPart && part.sections.empty())) { invalidKeywordLocation( block, "A part closes only after node, element, and matching 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; } if (modelElementFamily_ == ElementFamily::shell) { inputFailure( "unsupported-mixed-element-model", block.location, block.canonicalName, "", "Beam-section semantics cannot be mixed with shell elements."); return; } parseBeamSection(block, part); partSectionsSeen_ = !failure_; beamSectionContextActive_ = !failure_; } else if (block.canonicalName == "SHELL SECTION") { beamSectionContextActive_ = false; if (!partElementsSeen_) { invalidKeywordLocation( block, "SHELL SECTION follows the part mesh and optional sets."); return; } if (modelElementFamily_ == ElementFamily::beam) { inputFailure( "unsupported-mixed-element-model", block.location, block.canonicalName, "", "Shell-section semantics cannot be mixed with beam elements."); return; } parseShellSection(block, part); partSectionsSeen_ = !failure_; } else if (block.canonicalName == "SECTION POINTS") { parseSectionPoints(block, part); } else if (block.canonicalName == "TRANSVERSE SHEAR STIFFNESS") { 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; } RawElement::Type elementType{}; ElementFamily elementFamily{}; std::size_t expectedFieldCount = 0U; if (equalName(*type, "B33")) { elementType = RawElement::Type::b33; elementFamily = ElementFamily::beam; expectedFieldCount = 3U; } else if (equalName(*type, "S4")) { elementType = RawElement::Type::s4; elementFamily = ElementFamily::shell; expectedFieldCount = 5U; } else if (equalName(*type, "S4R")) { elementType = RawElement::Type::s4r; elementFamily = ElementFamily::shell; expectedFieldCount = 5U; } else { inputFailure( "unsupported-element-formulation", block.location, block.canonicalName, *type, "Only TYPE=B33, TYPE=S4, and TYPE=S4R belong to the approved element subsets."); return; } if (modelElementFamily_ && *modelElementFamily_ != elementFamily) { inputFailure( "unsupported-mixed-element-model", block.location, block.canonicalName, *type, "Beam and shell elements cannot be mixed in one model."); return; } modelElementFamily_ = elementFamily; if (block.data.empty()) { inputFailure( elementFamily == ElementFamily::shell ? "invalid-shell-connectivity" : "invalid-data-arity", block.location, block.canonicalName, "", "ELEMENT requires at least one row with the approved connectivity arity."); return; } for (const auto& row : block.data) { if (row.fields.size() != expectedFieldCount) { inputFailure( elementFamily == ElementFamily::shell ? "invalid-shell-connectivity" : "invalid-data-arity", row.location, block.canonicalName, "", elementFamily == ElementFamily::shell ? "S4 and S4R rows require a label and exactly four nodes." : "B33 rows require label, node 1, node 2."); return; } RawElement element{}; element.labelText = row.fields[0]; element.type = elementType; element.location = row.location; if (!parseInteger( row.fields[0], element.label, row.location, block.canonicalName, true)) { 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 distinctNodes{ element.nodeLabels.begin(), element.nodeLabels.end()}; if (distinctNodes.size() != element.nodeLabels.size()) { inputFailure( "invalid-shell-connectivity", row.location, block.canonicalName, element.labelText, "Shell connectivity requires four distinct source nodes."); return; } } if (std::any_of( part.elements.begin(), part.elements.end(), [&element](const RawElement& existing) { 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)); } } void parseShellSection(const KeywordBlock& block, RawPart& part) { for (const auto& candidate : block.parameters) { if (candidate.name != "ELSET" && candidate.name != "MATERIAL") { inputFailure( "unsupported-shell-section-option", block.location, block.canonicalName, candidate.name, "The shell-section option is outside the centered single-layer subset."); return; } } if (!validateParameters(block, {"ELSET", "MATERIAL"})) { return; } const auto* elementSet = requiredParameterValue(block, "ELSET"); const auto* material = requiredParameterValue(block, "MATERIAL"); if (elementSet == nullptr || material == nullptr) { return; } if (block.data.size() != 1U || block.data[0].fields.empty() || block.data[0].fields.size() > 2U) { inputFailure( "unsupported-shell-section-option", block.location, block.canonicalName, *elementSet, "SHELL SECTION requires one thickness row with one optional integration-point field."); return; } double thickness = 0.0; if (!parseModelDouble( block.data[0].fields[0], thickness, block.data[0].location, block.canonicalName, "invalid-shell-thickness", "Shell thickness must be finite and positive.")) { return; } if (!(thickness > 0.0)) { modelFailure( "invalid-shell-thickness", block.data[0].location, block.canonicalName, *elementSet, "Shell thickness must be finite and positive."); return; } if (block.data[0].fields.size() == 2U) { std::int64_t ignoredIntegrationPoints = 0; if (!tryPositiveInteger( block.data[0].fields[1], ignoredIntegrationPoints)) { inputFailure( "unsupported-shell-section-option", block.data[0].location, block.canonicalName, block.data[0].fields[1], "The optional shell integration-point field must be a positive integer."); return; } } part.shellSections.push_back( {*elementSet, *material, thickness, block.location}); } bool parseSetMembers( const KeywordBlock& block, bool generate, std::vector& 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 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 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, modelElementFamily_ == ElementFamily::shell ? "invalid-shell-material" : "invalid-beam-property", "Elastic material values must be finite.") || !parseModelDouble( block.data[0].fields[1], material.poissonRatio, block.data[0].location, block.canonicalName, modelElementFamily_ == ElementFamily::shell ? "invalid-shell-material" : "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& 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(first), static_cast(last), value, row.location}); } } void parseCload(const KeywordBlock& block, std::vector& 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(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")) { if (modelElementFamily_ == ElementFamily::shell) { inputFailure( "unsupported-nonlinear-geometry", block.location, block.canonicalName, *nlgeom->value, "Only absent NLGEOM or NLGEOM=NO is supported."); } else { modelFailure( "unsupported-nonlinear-geometry", block.location, block.canonicalName, *nlgeom->value, "Only absent NLGEOM or NLGEOM=NO is supported."); } return; } } 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) { if (block.canonicalName == "DLOAD" && modelElementFamily_ == ElementFamily::shell) { inputFailure( "unsupported-distributed-load", block.location, block.canonicalName, "", "Distributed, pressure, gravity, body, edge, and follower loads are unsupported."); return; } inputFailure( "unsupported-keyword", block.location, block.canonicalName, "", "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 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(index); } } return std::nullopt; } const RawSet* findSet( const std::vector& 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; } 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 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()); partShellSectionAssignments_.resize(parts_.size()); const bool shellModel = modelElementFamily_ == ElementFamily::shell; for (std::size_t partIndex = 0U; partIndex < parts_.size(); ++partIndex) { const RawPart& part = parts_[partIndex]; if (part.nodes.empty() || part.elements.empty() || (!shellModel && part.sections.empty())) { inputFailure( "invalid-model-cardinality", part.location, "PART", part.name, "A part requires nodes, elements, and its approved section form."); 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); for (const auto nodeLabel : element.nodeLabels) { if (findNode(part, nodeLabel) == nullptr) { inputFailure( shellModel ? "invalid-shell-connectivity" : "unresolved-reference", element.location, "ELEMENT", element.labelText, "Element connectivity must resolve within its part."); return; } } } for (const auto& set : part.nodeSets) { 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)); 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(definition_.shellSections.size()); definition_.shellSections.push_back({ section.elementSetName, section.thickness, *materialIndex, section.location}); for (const auto elementLabel : elementSet->members) { if (!assignments.emplace( elementLabel, std::make_pair( sectionIndex, *materialIndex)).second) { inputFailure( "invalid-shell-section-assignment", section.location, "SHELL SECTION", std::to_string(elementLabel), "A shell element cannot receive multiple section assignments."); return; } } } for (const auto& element : part.elements) { if (assignments.find(element.label) == assignments.end()) { inputFailure( "invalid-shell-section-assignment", element.location, "ELEMENT", element.labelText, "Every shell element requires exactly one resolved section assignment."); return; } } continue; } auto& assignments = partSectionAssignments_[partIndex]; for (const auto& section : part.sections) { const auto* elementSet = findSet( 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(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(&part - parts_.data()); } bool validateGeometry( const RawElement& raw, const Node& first, const Node& second, const GeneralBeamSection& section) { const auto norm = [](const std::array& vector) { return std::hypot(vector[0], vector[1], vector[2]); }; const auto maximumAbsolute = [](const std::array& 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 firstScaled{}; std::array secondScaled{}; std::array 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 tangent{ deltaScaled[0] / lengthRatio, deltaScaled[1] / lengthRatio, deltaScaled[2] / lengthRatio}; const double globalGuideScale = std::max(1.0, maximumAbsolute(section.firstAxis)); std::array 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 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 nodeIndices; std::map 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::max()) { inputFailure( "entity-index-overflow", rawNode.location, "NODE", rawNode.labelText, "Expanded node count exceeds EntityIndex capacity."); return; } const EntityIndex index = static_cast(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}); } for (const auto& rawElement : part->elements) { EntityIndex index = 0U; if (rawElement.type == RawElement::Type::b33) { const auto first = nodeIndices.find(rawElement.nodeLabels[0]); const auto second = nodeIndices.find(rawElement.nodeLabels[1]); const auto& assignments = partSectionAssignments_[partIndex(*part)]; const auto assignment = assignments.find(rawElement.label); if (first == nodeIndices.end() || second == nodeIndices.end() || assignment == assignments.end()) { inputFailure( "unresolved-reference", rawElement.location, "ELEMENT", rawElement.labelText, "Expanded connectivity and section assignment must resolve."); return; } if (definition_.elements.size() > std::numeric_limits::max()) { inputFailure( "entity-index-overflow", rawElement.location, "ELEMENT", rawElement.labelText, "Expanded element count exceeds EntityIndex capacity."); return; } index = static_cast(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 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::max()) { inputFailure( "entity-index-overflow", rawElement.location, "ELEMENT", rawElement.labelText, "Expanded shell element count exceeds EntityIndex capacity."); return; } index = static_cast(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}); } 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> resolveNodeTarget( const std::string& target, const SourceLocation& location, const std::string& keyword) { std::vector matchingSets; for (const auto& set : definition_.nodeSets) { if (equalName(set.name, target)) { matchingSets.push_back(&set); } } std::vector 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(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 boundaries = modelBoundaries_; boundaries.insert( boundaries.end(), step_.boundaries.begin(), step_.boundaries.end()); std::map, 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 failure_; std::vector parts_; std::vector instances_; std::vector assemblySets_; std::vector materials_; std::vector modelBoundaries_; RawStep step_{}; std::vector>> partSectionAssignments_; std::vector>> partShellSectionAssignments_; std::optional currentPart_; std::optional pendingSection_; std::optional materialEligible_; std::optional modelElementFamily_; 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 AbaqusDomainMapper::map(const ParsedInput& input) const { return MappingContext{input}.run(); } } // namespace fesa