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

2176 lines
83 KiB
C++

#include "fesa/io/abaqus/domain_mapper.h"
#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>
#include "fesa/core/ascii.h"
#include "fesa/math/vector3.h"
#include "fesa/model/shell_geometry.h"
#include "fesa/model/source_target_resolver.h"
namespace fesa {
namespace {
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 label_text;
std::array<double, 3> coordinates;
SourceLocation location;
};
struct RawElement {
enum class Type { kB33, kS4, kS4r };
std::int64_t label;
std::string label_text;
Type type;
std::vector<std::int64_t> node_labels;
SourceLocation location;
};
enum class ElementFamily { kBeam, kShell };
struct RawSet {
std::string name;
std::vector<std::int64_t> members;
SourceLocation location;
};
struct RawSection {
std::string element_set_name;
std::string material_name;
std::array<double, 5> properties;
std::array<double, 3> first_axis;
std::vector<std::array<double, 2>> section_points;
SourceLocation location;
};
struct RawShellSection {
std::string element_set_name;
std::string material_name;
double thickness;
SourceLocation location;
};
struct RawPart {
std::string name;
SourceLocation location;
std::vector<RawNode> nodes;
std::vector<RawElement> elements;
std::vector<RawSet> node_sets;
std::vector<RawSet> element_sets;
std::vector<RawSection> sections;
std::vector<RawShellSection> shell_sections;
};
struct RawInstance {
std::string name;
std::string part_name;
SourceLocation location;
};
struct RawAssemblySet {
bool is_node_set;
std::string name;
std::string instance_name;
std::vector<std::int64_t> members;
SourceLocation location;
};
struct RawMaterial {
std::string name;
double youngs_modulus{0.0};
double poisson_ratio{0.0};
bool has_elastic{false};
SourceLocation location;
SourceLocation elastic_location;
};
struct RawStep {
SourceLocation location;
bool has_static{false};
std::array<double, 4> static_values{};
std::vector<BoundaryCondition> boundaries;
std::vector<NodalLoad> loads;
};
struct MappingFailure {
FailureCategory category;
Diagnostic diagnostic;
};
/// @brief Builds one complete Domain candidate while retaining source identity.
/// @note The candidate is committed only after all blocks and cross-references
/// have been validated.
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 entity_identity, std::string message) {
if (!failure_) {
failure_ = MappingFailure{
category,
{Severity::kError, std::move(code), location, std::move(keyword),
std::move(entity_identity), std::move(message)}};
}
return false;
}
bool InputFailure(std::string code, const SourceLocation& location,
std::string keyword, std::string entity_identity,
std::string message) {
return Fail(FailureCategory::kInput, std::move(code), location,
std::move(keyword), std::move(entity_identity),
std::move(message));
}
bool ModelFailure(std::string code, const SourceLocation& location,
std::string keyword, std::string entity_identity,
std::string message) {
return Fail(FailureCategory::kModel, std::move(code), location,
std::move(keyword), std::move(entity_identity),
std::move(message));
}
bool InvalidKeywordLocation(const KeywordBlock& block, std::string message) {
return InputFailure("invalid-keyword-location", block.location,
block.canonical_name, "", 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.canonical_name, 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.canonical_name,
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.canonical_name, 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.canonical_name, "",
"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 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& diagnostic_code,
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(diagnostic_code, 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 AsciiCaseInsensitiveEquals(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 AsciiCaseInsensitiveEquals(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 AsciiCaseInsensitiveEquals(value.name, name);
});
}
bool ContainsSetName(const std::vector<RawSet>& sets,
const std::string& name) const {
const auto matches = [&name](const RawSet& set) {
return AsciiCaseInsensitiveEquals(set.name, name);
};
return std::any_of(sets.begin(), sets.end(), matches);
}
bool ContainsAssemblySetName(bool node_set, const std::string& name) const {
return std::any_of(assembly_sets_.begin(), assembly_sets_.end(),
[node_set, &name](const RawAssemblySet& set) {
return set.is_node_set == node_set &&
AsciiCaseInsensitiveEquals(set.name, name);
});
}
void ParseBlocks() {
definition_.source_path = input_.source_path;
definition_.source_content_identity = input_.source_content_identity;
for (std::size_t index = 0U; index < input_.blocks.size() && !failure_;
++index) {
const auto& block = input_.blocks[index];
if (in_instance_) {
ParseInstanceBlock(block);
} else if (current_part_) {
ParsePartBlock(block);
} else if (in_assembly_) {
ParseAssemblyBlock(block);
} else if (in_step_) {
ParseStepBlock(block);
} else {
ParseTopLevelBlock(block, index);
}
}
if (!failure_ &&
(current_part_ || in_assembly_ || in_instance_ || in_step_)) {
const auto location = input_.blocks.empty()
? SourceLocation{input_.source_path, 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 (step_seen_) {
if (block.canonical_name == "STEP") {
ParseStepStart(block);
} else {
InvalidKeywordLocation(
block, "The sole analysis step must be the final top-level block.");
}
return;
}
if (block.canonical_name != "ELASTIC") {
material_eligible_.reset();
}
pending_section_.reset();
active_output_ = false;
if (block.canonical_name == "HEADING") {
if (index != 0U || heading_seen_ || !ValidateParameters(block, {})) {
if (!failure_) {
InputFailure("invalid-keyword-location", block.location,
block.canonical_name, "",
"HEADING is optional only as the first keyword.");
}
return;
}
heading_seen_ = 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.canonical_name == "PREPRINT") {
if (!RequireNoData(block)) {
return;
}
AddIgnoredWarning(block);
return;
}
if (block.canonical_name == "PART") {
ParsePartStart(block);
return;
}
if (block.canonical_name == "ASSEMBLY") {
ParseAssemblyStart(block);
return;
}
if (block.canonical_name == "MATERIAL") {
ParseMaterial(block);
return;
}
if (block.canonical_name == "ELASTIC") {
ParseElastic(block);
return;
}
if (block.canonical_name == "BOUNDARY") {
if (!assembly_seen_ || materials_.empty()) {
InvalidKeywordLocation(
block, "Model boundary data follows the assembly and materials.");
return;
}
model_boundary_seen_ = true;
ParseBoundary(block, model_boundaries_);
return;
}
if (block.canonical_name == "STEP") {
ParseStepStart(block);
return;
}
if (block.canonical_name == "END PART" ||
block.canonical_name == "END ASSEMBLY" ||
block.canonical_name == "END INSTANCE" ||
block.canonical_name == "END STEP") {
InputFailure("invalid-keyword-location", block.location,
block.canonical_name, "",
"The closing keyword has no matching open block.");
return;
}
RejectUnknown(block);
}
void ParsePartStart(const KeywordBlock& block) {
if (assembly_seen_ || !materials_.empty() || model_boundary_seen_) {
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.canonical_name,
*name,
"Part names are unique under case-insensitive lookup.");
return;
}
parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}});
current_part_ = parts_.size() - 1U;
part_elements_seen_ = false;
part_sets_seen_ = false;
part_sections_seen_ = false;
beam_section_context_active_ = false;
}
void ParsePartBlock(const KeywordBlock& block) {
if (block.canonical_name == "END PART") {
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
return;
}
const RawPart& part = parts_[*current_part_];
const bool shell_part = model_element_family_ == ElementFamily::kShell;
if (part.nodes.empty() || part.elements.empty() ||
(!shell_part && part.sections.empty())) {
InvalidKeywordLocation(block,
"A part closes only after node, element, "
"and matching section blocks.");
return;
}
current_part_.reset();
pending_section_.reset();
beam_section_context_active_ = false;
return;
}
if (block.canonical_name == "PART") {
InputFailure("invalid-keyword-location", block.location,
block.canonical_name, "",
"Nested part blocks are not supported.");
return;
}
if (block.canonical_name == "ASSEMBLY") {
InputFailure("unsupported-nested-assembly", block.location,
block.canonical_name, "",
"An assembly cannot be nested in a part.");
return;
}
RawPart& part = parts_[*current_part_];
if (block.canonical_name == "NODE") {
beam_section_context_active_ = false;
if (part_elements_seen_ || part_sets_seen_ || part_sections_seen_) {
InvalidKeywordLocation(
block,
"NODE blocks must precede element, set, and section blocks.");
return;
}
pending_section_.reset();
ParseNodes(block, part);
} else if (block.canonical_name == "ELEMENT") {
beam_section_context_active_ = false;
if (part.nodes.empty() || part_sets_seen_ || part_sections_seen_) {
InvalidKeywordLocation(block,
"ELEMENT blocks follow at least one NODE block "
"and precede sets and sections.");
return;
}
pending_section_.reset();
ParseElements(block, part);
part_elements_seen_ = !failure_;
} else if (block.canonical_name == "NSET") {
beam_section_context_active_ = false;
if (!part_elements_seen_ || part_sections_seen_) {
InvalidKeywordLocation(
block,
"Part sets follow element blocks and precede beam sections.");
return;
}
pending_section_.reset();
ParseSet(block, true, part);
part_sets_seen_ = !failure_;
} else if (block.canonical_name == "ELSET") {
beam_section_context_active_ = false;
if (!part_elements_seen_ || part_sections_seen_) {
InvalidKeywordLocation(
block,
"Part sets follow element blocks and precede beam sections.");
return;
}
pending_section_.reset();
ParseSet(block, false, part);
part_sets_seen_ = !failure_;
} else if (block.canonical_name == "BEAM GENERAL SECTION") {
if (!part_elements_seen_) {
InvalidKeywordLocation(
block,
"BEAM GENERAL SECTION follows the part mesh and optional sets.");
return;
}
if (model_element_family_ == ElementFamily::kShell) {
InputFailure(
"unsupported-mixed-element-model", block.location,
block.canonical_name, "",
"Beam-section semantics cannot be mixed with shell elements.");
return;
}
ParseBeamSection(block, part);
part_sections_seen_ = !failure_;
beam_section_context_active_ = !failure_;
} else if (block.canonical_name == "SHELL SECTION") {
beam_section_context_active_ = false;
if (!part_elements_seen_) {
InvalidKeywordLocation(
block, "SHELL SECTION follows the part mesh and optional sets.");
return;
}
if (model_element_family_ == ElementFamily::kBeam) {
InputFailure(
"unsupported-mixed-element-model", block.location,
block.canonical_name, "",
"Shell-section semantics cannot be mixed with beam elements.");
return;
}
ParseShellSection(block, part);
part_sections_seen_ = !failure_;
} else if (block.canonical_name == "SECTION POINTS") {
ParseSectionPoints(block, part);
} else if (block.canonical_name == "TRANSVERSE SHEAR STIFFNESS") {
pending_section_.reset();
if (!beam_section_context_active_) {
InputFailure(
"invalid-keyword-location", block.location, block.canonical_name,
"",
"The ignored shear keyword still requires beam-section context.");
return;
}
AddIgnoredWarning(block);
} else {
pending_section_.reset();
beam_section_context_active_ = 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.canonical_name,
"", "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.canonical_name,
"", "NODE rows require label, x, y, z.");
return;
}
RawNode node{};
node.label_text = row.fields[0];
node.location = row.location;
if (!ParseInteger(row.fields[0], node.label, row.location,
block.canonical_name, 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.canonical_name,
node.label_text, "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.canonical_name)) {
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 element_type{};
ElementFamily element_family{};
std::size_t expected_field_count = 0U;
if (AsciiCaseInsensitiveEquals(*type, "B33")) {
element_type = RawElement::Type::kB33;
element_family = ElementFamily::kBeam;
expected_field_count = 3U;
} else if (AsciiCaseInsensitiveEquals(*type, "S4")) {
element_type = RawElement::Type::kS4;
element_family = ElementFamily::kShell;
expected_field_count = 5U;
} else if (AsciiCaseInsensitiveEquals(*type, "S4R")) {
element_type = RawElement::Type::kS4r;
element_family = ElementFamily::kShell;
expected_field_count = 5U;
} else {
InputFailure(
"unsupported-element-formulation", block.location,
block.canonical_name, *type,
"Only TYPE=B33, TYPE=S4, and TYPE=S4R belong to the approved "
"element subsets.");
return;
}
if (model_element_family_ && *model_element_family_ != element_family) {
InputFailure("unsupported-mixed-element-model", block.location,
block.canonical_name, *type,
"Beam and shell elements cannot be mixed in one model.");
return;
}
model_element_family_ = element_family;
if (block.data.empty()) {
InputFailure(element_family == ElementFamily::kShell
? "invalid-shell-connectivity"
: "invalid-data-arity",
block.location, block.canonical_name, "",
"ELEMENT requires at least one row with the approved "
"connectivity arity.");
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != expected_field_count) {
InputFailure(
element_family == ElementFamily::kShell
? "invalid-shell-connectivity"
: "invalid-data-arity",
row.location, block.canonical_name, "",
element_family == ElementFamily::kShell
? "S4 and S4R rows require a label and exactly four nodes."
: "B33 rows require label, node 1, node 2.");
return;
}
RawElement element{};
element.label_text = row.fields[0];
element.type = element_type;
element.location = row.location;
if (!ParseInteger(row.fields[0], element.label, row.location,
block.canonical_name, true)) {
return;
}
element.node_labels.reserve(expected_field_count - 1U);
for (std::size_t field = 1U; field < expected_field_count; ++field) {
std::int64_t node_label = 0;
if (!ParseInteger(row.fields[field], node_label, row.location,
block.canonical_name, true)) {
return;
}
element.node_labels.push_back(node_label);
}
if (element_family == ElementFamily::kShell) {
const std::set<std::int64_t> distinct_nodes{element.node_labels.begin(),
element.node_labels.end()};
if (distinct_nodes.size() != element.node_labels.size()) {
InputFailure(
"invalid-shell-connectivity", row.location, block.canonical_name,
element.label_text,
"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.canonical_name,
element.label_text,
"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.canonical_name, candidate.name,
"The shell-section option is outside the centered "
"single-layer subset.");
return;
}
}
if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) {
return;
}
const auto* element_set = RequiredParameterValue(block, "ELSET");
const auto* material = RequiredParameterValue(block, "MATERIAL");
if (element_set == 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.canonical_name, *element_set,
"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.canonical_name,
"invalid-shell-thickness",
"Shell thickness must be finite and positive.")) {
return;
}
if (!(thickness > 0.0)) {
ModelFailure("invalid-shell-thickness", block.data[0].location,
block.canonical_name, *element_set,
"Shell thickness must be finite and positive.");
return;
}
if (block.data[0].fields.size() == 2U) {
if (!ParsePositiveSourceLabel(block.data[0].fields[1]).HasValue()) {
InputFailure("unsupported-shell-section-option", block.data[0].location,
block.canonical_name, block.data[0].fields[1],
"The optional shell integration-point field must be a "
"positive integer.");
return;
}
}
part.shell_sections.push_back(
{*element_set, *material, thickness, block.location});
}
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.canonical_name, "",
"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.canonical_name, "",
"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.canonical_name,
true) ||
!ParseInteger(fields[1], last, data.location, block.canonical_name,
true)) {
return false;
}
if (!ParseInteger(fields[2], increment, data.location,
block.canonical_name, false) ||
increment <= 0 || first > last || (last - first) % increment != 0) {
return InputFailure("invalid-set-range", data.location,
block.canonical_name, "",
"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.canonical_name, "",
"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.canonical_name,
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.canonical_name, std::to_string(member),
"A set cannot repeat the same source member.");
}
}
return true;
}
void ParseSet(const KeywordBlock& block, bool node_set, RawPart& part) {
const std::string_view name_parameter = node_set ? "NSET" : "ELSET";
if (!ValidateParameters(block, {name_parameter, "GENERATE"})) {
return;
}
const auto* name = RequiredParameterValue(block, name_parameter);
if (name == nullptr) {
return;
}
const auto* generate_parameter = Parameter(block, "GENERATE");
if (generate_parameter != nullptr && generate_parameter->value) {
InputFailure("invalid-keyword-parameter", block.location,
block.canonical_name, "GENERATE",
"GENERATE is a valueless flag.");
return;
}
const auto& sets = node_set ? part.node_sets : part.element_sets;
if (ContainsSetName(sets, *name)) {
InputFailure("duplicate-entity", block.location, block.canonical_name,
*name,
"Set names are unique within their node-set or element-set "
"namespace.");
return;
}
RawSet set{*name, {}, block.location};
if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) {
return;
}
if (node_set) {
part.node_sets.push_back(std::move(set));
} else {
part.element_sets.push_back(std::move(set));
}
}
void ParseBeamSection(const KeywordBlock& block, RawPart& part) {
pending_section_.reset();
if (!ValidateParameters(block, {"ELSET", "MATERIAL", "SECTION"})) {
return;
}
const auto* element_set = RequiredParameterValue(block, "ELSET");
const auto* material = RequiredParameterValue(block, "MATERIAL");
const auto* section = RequiredParameterValue(block, "SECTION");
if (element_set == nullptr || material == nullptr || section == nullptr) {
return;
}
if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) {
InputFailure("unsupported-section-formulation", block.location,
block.canonical_name, *section,
"Only SECTION=GENERAL belongs to the approved subset.");
return;
}
if (std::any_of(part.sections.begin(), part.sections.end(),
[element_set](const RawSection& existing) {
return AsciiCaseInsensitiveEquals(
existing.element_set_name, *element_set);
})) {
InputFailure("duplicate-entity", block.location, block.canonical_name,
*element_set,
"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.canonical_name,
*element_set,
"A general section requires one five-field property row and "
"one three-field axis row.");
return;
}
RawSection raw{*element_set, *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.canonical_name, "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.first_axis[coordinate], block.data[1].location,
block.canonical_name, "invalid-beam-guide-vector",
"The beam guide vector must be finite.")) {
return;
}
}
part.sections.push_back(std::move(raw));
pending_section_ = part.sections.size() - 1U;
}
void ParseSectionPoints(const KeywordBlock& block, RawPart& part) {
if (!pending_section_ || !ValidateParameters(block, {}) ||
block.data.empty()) {
if (!failure_) {
InputFailure(
"invalid-keyword-location", block.location, block.canonical_name,
"", "SECTION POINTS must immediately follow a general section.");
}
return;
}
RawSection& section = part.sections[*pending_section_];
for (const auto& row : block.data) {
if (row.fields.size() != 2U) {
InputFailure("invalid-data-arity", row.location, block.canonical_name,
section.element_set_name,
"Section-point rows require x1 and x2.");
return;
}
std::array<double, 2> point{};
if (!ParseDouble(row.fields[0], point[0], row.location,
block.canonical_name) ||
!ParseDouble(row.fields[1], point[1], row.location,
block.canonical_name)) {
return;
}
if (std::find(section.section_points.begin(),
section.section_points.end(),
point) != section.section_points.end()) {
InputFailure("duplicate-entity", row.location, block.canonical_name,
section.element_set_name,
"Section points must be unique within a section.");
return;
}
section.section_points.push_back(point);
}
pending_section_.reset();
}
void ParseAssemblyStart(const KeywordBlock& block) {
if (assembly_seen_) {
InputFailure("unsupported-nested-assembly", block.location,
block.canonical_name, "",
"V0 accepts exactly one non-nested assembly.");
return;
}
if (parts_.empty() || !materials_.empty() || model_boundary_seen_) {
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;
}
assembly_seen_ = true;
in_assembly_ = true;
assembly_set_seen_ = false;
}
void ParseAssemblyBlock(const KeywordBlock& block) {
if (block.canonical_name == "END ASSEMBLY") {
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
return;
}
if (instances_.empty()) {
InvalidKeywordLocation(
block, "The assembly requires at least one identity instance.");
return;
}
in_assembly_ = false;
return;
}
if (block.canonical_name == "ASSEMBLY") {
InputFailure("unsupported-nested-assembly", block.location,
block.canonical_name, "",
"Nested or duplicate assembly blocks are unsupported.");
} else if (block.canonical_name == "INSTANCE") {
if (assembly_set_seen_) {
InvalidKeywordLocation(
block, "All identity instances must precede assembly-level sets.");
return;
}
ParseInstanceStart(block);
} else if (block.canonical_name == "NSET") {
if (instances_.empty()) {
InvalidKeywordLocation(
block, "Assembly sets follow at least one identity instance.");
return;
}
assembly_set_seen_ = true;
ParseAssemblySet(block, true);
} else if (block.canonical_name == "ELSET") {
if (instances_.empty()) {
InvalidKeywordLocation(
block, "Assembly sets follow at least one identity instance.");
return;
}
assembly_set_seen_ = 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.canonical_name, "",
"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.canonical_name, *name,
"Instance translation or rotation data is unsupported.");
return;
}
if (ContainsName(instances_, *name)) {
InputFailure("duplicate-entity", block.location, block.canonical_name,
*name, "Instance names are globally unique.");
return;
}
instances_.push_back({*name, *part, block.location});
in_instance_ = true;
}
void ParseInstanceBlock(const KeywordBlock& block) {
if (block.canonical_name == "END INSTANCE") {
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
return;
}
in_instance_ = false;
return;
}
if (block.canonical_name == "ASSEMBLY") {
InputFailure("unsupported-nested-assembly", block.location,
block.canonical_name, "",
"An assembly cannot be nested in an instance.");
return;
}
InputFailure("unsupported-instance-mesh-semantics", block.location,
block.canonical_name, instances_.back().name,
"Instance-local mesh definitions are unsupported.");
}
void ParseAssemblySet(const KeywordBlock& block, bool node_set) {
const std::string_view name_parameter = node_set ? "NSET" : "ELSET";
if (!ValidateParameters(block, {name_parameter, "INSTANCE", "GENERATE"})) {
return;
}
const auto* name = RequiredParameterValue(block, name_parameter);
const auto* instance = RequiredParameterValue(block, "INSTANCE");
if (name == nullptr || instance == nullptr) {
return;
}
const auto* generate_parameter = Parameter(block, "GENERATE");
if (generate_parameter != nullptr && generate_parameter->value) {
InputFailure("invalid-keyword-parameter", block.location,
block.canonical_name, "GENERATE",
"GENERATE is a valueless flag.");
return;
}
if (ContainsAssemblySetName(node_set, *name)) {
InputFailure("duplicate-entity", block.location, block.canonical_name,
*name,
"Assembly set names are unique within their node-set or "
"element-set namespace.");
return;
}
RawAssemblySet set{node_set, *name, *instance, {}, block.location};
if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) {
return;
}
assembly_sets_.push_back(std::move(set));
}
void ParseMaterial(const KeywordBlock& block) {
if (!assembly_seen_ || model_boundary_seen_) {
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.canonical_name,
*name, "Material names are globally unique.");
return;
}
materials_.push_back({*name, 0.0, 0.0, false, block.location, {}});
material_eligible_ = materials_.size() - 1U;
}
void ParseElastic(const KeywordBlock& block) {
if (!material_eligible_) {
InputFailure("invalid-keyword-location", block.location,
block.canonical_name, "",
"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.canonical_name,
materials_[*material_eligible_].name,
"ELASTIC requires exactly one E, nu row.");
}
return;
}
RawMaterial& material = materials_[*material_eligible_];
if (material.has_elastic) {
InputFailure("duplicate-entity", block.location, block.canonical_name,
material.name, "A material accepts one ELASTIC definition.");
return;
}
if (!ParseModelDouble(block.data[0].fields[0], material.youngs_modulus,
block.data[0].location, block.canonical_name,
model_element_family_ == ElementFamily::kShell
? "invalid-shell-material"
: "invalid-beam-property",
"Elastic material values must be finite.") ||
!ParseModelDouble(block.data[0].fields[1], material.poisson_ratio,
block.data[0].location, block.canonical_name,
model_element_family_ == ElementFamily::kShell
? "invalid-shell-material"
: "invalid-beam-property",
"Elastic material values must be finite.")) {
return;
}
material.has_elastic = true;
material.elastic_location = 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.canonical_name,
"", "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.canonical_name,
"",
"BOUNDARY rows require target, first DOF, last DOF, and "
"optional value.");
return;
}
if (row.fields[0].empty()) {
InputFailure("unresolved-reference", row.location, block.canonical_name,
"", "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.canonical_name, true) ||
!ParseInteger(row.fields[2], last, row.location, block.canonical_name,
true)) {
return;
}
if (first < 1 || first > 6 || last < first || last > 6) {
InputFailure("invalid-dof", row.location, block.canonical_name,
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.canonical_name)) {
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.canonical_name,
"", "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.canonical_name,
"", "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.canonical_name,
true) ||
!ParseDouble(row.fields[2], magnitude, row.location,
block.canonical_name)) {
return;
}
if (dof < 1 || dof > 6) {
InputFailure("invalid-dof", row.location, block.canonical_name,
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 (step_seen_) {
InputFailure("unsupported-multiple-step", block.location,
block.canonical_name, "",
"V0 accepts exactly one analysis step.");
return;
}
if (!assembly_seen_ || 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.canonical_name, "NLGEOM",
"NLGEOM requires NO in the approved subset.");
return;
}
if (!AsciiCaseInsensitiveEquals(*nlgeom->value, "NO")) {
if (model_element_family_ == ElementFamily::kShell) {
InputFailure("unsupported-nonlinear-geometry", block.location,
block.canonical_name, *nlgeom->value,
"Only absent NLGEOM or NLGEOM=NO is supported.");
} else {
ModelFailure("unsupported-nonlinear-geometry", block.location,
block.canonical_name, *nlgeom->value,
"Only absent NLGEOM or NLGEOM=NO is supported.");
}
return;
}
}
step_seen_ = true;
in_step_ = true;
step_ = RawStep{block.location, false, {}, {}, {}};
step_load_seen_ = false;
step_no_op_seen_ = false;
}
void ParseStepBlock(const KeywordBlock& block) {
if (block.canonical_name == "END STEP") {
active_output_ = false;
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
return;
}
if (!step_.has_static) {
InvalidKeywordLocation(
block,
"The sole step requires exactly one leading STATIC procedure.");
return;
}
in_step_ = false;
return;
}
if (block.canonical_name == "STEP") {
InputFailure("unsupported-multiple-step", block.location,
block.canonical_name, "",
"A second or nested step is unsupported.");
return;
}
if (block.canonical_name == "STATIC") {
active_output_ = false;
if (!step_.boundaries.empty() || !step_.loads.empty() ||
step_no_op_seen_) {
InvalidKeywordLocation(
block, "STATIC must be the first keyword in the sole step.");
return;
}
ParseStatic(block);
} else if (block.canonical_name == "BOUNDARY") {
active_output_ = false;
if (!step_.has_static || step_load_seen_ || step_no_op_seen_) {
InvalidKeywordLocation(
block,
"Step boundaries follow STATIC and precede loads "
"and no-op requests.");
return;
}
ParseBoundary(block, step_.boundaries);
} else if (block.canonical_name == "CLOAD") {
active_output_ = false;
if (!step_.has_static || step_no_op_seen_) {
InvalidKeywordLocation(
block,
"CLOAD follows STATIC and boundaries and precedes no-op requests.");
return;
}
ParseCload(block, step_.loads);
step_load_seen_ = !failure_;
} else if (block.canonical_name == "RESTART") {
active_output_ = false;
if (!step_.has_static) {
InvalidKeywordLocation(
block, "Step no-op requests follow STATIC, boundaries, and loads.");
return;
}
step_no_op_seen_ = true;
if (RequireNoData(block)) {
AddIgnoredWarning(block);
}
} else if (block.canonical_name == "OUTPUT") {
if (!step_.has_static) {
InvalidKeywordLocation(
block, "Step no-op requests follow STATIC, boundaries, and loads.");
return;
}
step_no_op_seen_ = true;
ParseOutputRoot(block);
} else if (block.canonical_name == "NODE OUTPUT" ||
block.canonical_name == "ELEMENT OUTPUT" ||
block.canonical_name == "CONTACT OUTPUT") {
ParseOutputChild(block);
} else {
active_output_ = false;
RejectUnknown(block);
}
}
void ParseStatic(const KeywordBlock& block) {
if (step_.has_static || !ValidateParameters(block, {}) ||
block.data.size() != 1U || block.data[0].fields.size() != 4U) {
if (!failure_) {
InputFailure("invalid-static-data", block.location,
block.canonical_name, "",
"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_.static_values[field],
block.data[0].location, block.canonical_name)) {
return;
}
if (step_.static_values[field] <= 0.0) {
InputFailure("invalid-static-data", block.data[0].location,
block.canonical_name, "",
"All four STATIC fields must be positive.");
return;
}
}
if (step_.static_values[2] > step_.static_values[3]) {
InputFailure("invalid-static-data", block.data[0].location,
block.canonical_name, "",
"STATIC minimum increment cannot exceed maximum increment.");
return;
}
step_.has_static = 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.canonical_name,
"", "OUTPUT must select exactly FIELD or HISTORY.");
return;
}
active_output_ = true;
AddIgnoredWarning(block);
}
void ParseOutputChild(const KeywordBlock& block) {
if (!active_output_) {
InputFailure(
"invalid-keyword-location", block.location, block.canonical_name, "",
"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::kWarning, "ignored-input-keyword", block.location,
block.canonical_name, "",
"The allowlisted Abaqus keyword is ignored without semantic effect."});
}
void RejectUnknown(const KeywordBlock& block) {
if (block.canonical_name == "DLOAD" &&
model_element_family_ == ElementFamily::kShell) {
InputFailure("unsupported-distributed-load", block.location,
block.canonical_name, "",
"Distributed, pressure, gravity, body, edge, and follower "
"loads are unsupported.");
return;
}
InputFailure("unsupported-keyword", block.location, block.canonical_name,
"", "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 AsciiCaseInsensitiveEquals(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 AsciiCaseInsensitiveEquals(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 (AsciiCaseInsensitiveEquals(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 AsciiCaseInsensitiveEquals(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() || !assembly_seen_ || instances_.empty() ||
materials_.empty() || !step_seen_ || !step_.has_static) {
const auto location = input_.blocks.empty()
? SourceLocation{input_.source_path, 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;
}
if (!definition_.shell_elements.empty()) {
auto geometry =
PreprocessShellGeometry(definition_.nodes, definition_.shell_elements,
definition_.shell_sections);
if (!geometry.HasValue()) {
const auto& status = geometry.GetStatus();
failure_ =
MappingFailure{status.Category().value_or(FailureCategory::kModel),
status.Diagnostics().front()};
return;
}
definition_.shell_node_initial_frames =
std::move(geometry.Value().nodal_frames);
}
ExpandAssemblySets();
if (failure_) {
return;
}
FinalizeStep();
}
void FinalizeMaterials() {
for (const auto& material : materials_) {
if (!material.has_elastic) {
InputFailure("unresolved-reference", material.location, "MATERIAL",
material.name,
"A material must own exactly one ELASTIC row.");
return;
}
if (model_element_family_ == ElementFamily::kShell) {
if (!(material.youngs_modulus > 0.0) ||
!(material.poisson_ratio > -1.0) ||
!(material.poisson_ratio < 0.5)) {
ModelFailure(
"invalid-shell-material", material.elastic_location, "ELASTIC",
material.name,
"Shell isotropic elasticity requires E>0 and -1<nu<0.5.");
return;
}
definition_.materials.push_back({material.name, material.youngs_modulus,
material.poisson_ratio,
material.location});
continue;
}
const double denominator = 2.0 * (1.0 + material.poisson_ratio);
const double shear_modulus = material.youngs_modulus / denominator;
// The approved ledger deliberately validates derived G, not an
// independent upper bound on Poisson's ratio.
if (!(material.youngs_modulus > 0.0) || !std::isfinite(shear_modulus) ||
!(shear_modulus > 0.0)) {
ModelFailure("invalid-beam-property", material.elastic_location,
"ELASTIC", material.name,
"E and the derived G=E/(2*(1+nu)) must be positive.");
return;
}
definition_.materials.push_back({material.name, material.youngs_modulus,
material.poisson_ratio,
material.location});
}
}
void FinalizePartsAndSections() {
part_section_assignments_.resize(parts_.size());
part_shell_section_assignments_.resize(parts_.size());
const bool shell_model = model_element_family_ == ElementFamily::kShell;
for (std::size_t part_index = 0U; part_index < parts_.size();
++part_index) {
const RawPart& part = parts_[part_index];
if (part.nodes.empty() || part.elements.empty() ||
(!shell_model && part.sections.empty())) {
InputFailure("invalid-model-cardinality", part.location, "PART",
part.name,
"A part requires nodes, elements, and its approved "
"section form.");
return;
}
PartDefinition part_definition{};
part_definition.name = part.name;
part_definition.location = part.location;
for (const auto& node : part.nodes) {
part_definition.node_source_labels.push_back(node.label);
}
for (const auto& element : part.elements) {
part_definition.element_source_labels.push_back(element.label);
for (const auto node_label : element.node_labels) {
if (FindNode(part, node_label) == nullptr) {
InputFailure(shell_model ? "invalid-shell-connectivity"
: "unresolved-reference",
element.location, "ELEMENT", element.label_text,
"Element connectivity must resolve within its part.");
return;
}
}
}
for (const auto& set : part.node_sets) {
part_definition.node_set_names.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.element_sets) {
part_definition.element_set_names.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(part_definition));
if (shell_model) {
auto& assignments = part_shell_section_assignments_[part_index];
for (const auto& section : part.shell_sections) {
const auto* element_set =
FindSet(part.element_sets, section.element_set_name);
const auto material_index = FindMaterialIndex(section.material_name);
if (element_set == nullptr || !material_index) {
InputFailure("unresolved-shell-section", section.location,
"SHELL SECTION", section.element_set_name,
"Shell section ELSET and MATERIAL references "
"must resolve.");
return;
}
const EntityIndex section_index =
static_cast<EntityIndex>(definition_.shell_sections.size());
definition_.shell_sections.push_back(
{section.element_set_name, section.thickness, *material_index,
section.location});
for (const auto element_label : element_set->members) {
if (!assignments
.emplace(element_label,
std::make_pair(section_index, *material_index))
.second) {
InputFailure("invalid-shell-section-assignment", section.location,
"SHELL SECTION", std::to_string(element_label),
"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.label_text,
"Every shell element requires exactly one "
"resolved section assignment.");
return;
}
}
continue;
}
auto& assignments = part_section_assignments_[part_index];
for (const auto& section : part.sections) {
const auto* element_set =
FindSet(part.element_sets, section.element_set_name);
const auto material_index = FindMaterialIndex(section.material_name);
if (element_set == nullptr || !material_index) {
InputFailure("unresolved-reference", section.location,
"BEAM GENERAL SECTION", section.element_set_name,
"Section ELSET and MATERIAL references must resolve.");
return;
}
if (section.properties[2] != 0.0) {
ModelFailure("unsupported-coupled-section", section.location,
"BEAM GENERAL SECTION", section.element_set_name,
"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.element_set_name,
"A, I11, I22, and J must be positive.");
return;
}
const EntityIndex section_index =
static_cast<EntityIndex>(definition_.sections.size());
definition_.sections.push_back(
{section.element_set_name, section.properties[0],
section.properties[1], section.properties[2],
section.properties[3], section.properties[4], section.first_axis,
section.section_points, section.location});
for (const auto element_label : element_set->members) {
if (!assignments
.emplace(element_label,
std::make_pair(section_index, *material_index))
.second) {
InputFailure("duplicate-entity", section.location,
"BEAM GENERAL SECTION", std::to_string(element_label),
"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.label_text,
"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 maximum_absolute = [](const Vector3& vector) {
return std::max(
{std::abs(vector[0]), std::abs(vector[1]), std::abs(vector[2])});
};
const Vector3 first_position{first.coordinates};
const Vector3 second_position{second.coordinates};
// 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 global_coordinate_scale =
std::max({1.0, maximum_absolute(first_position),
maximum_absolute(second_position)});
const Vector3 first_scaled = first_position / global_coordinate_scale;
const Vector3 second_scaled = second_position / global_coordinate_scale;
const Vector3 delta_scaled = second_scaled - first_scaled;
const double length_ratio = delta_scaled.Norm();
const double coordinate_norm_ratio =
std::max({1.0 / global_coordinate_scale, first_scaled.Norm(),
second_scaled.Norm()});
if (!(length_ratio > 1.0e-12 * coordinate_norm_ratio)) {
return ModelFailure(
"invalid-beam-length", raw.location, "ELEMENT", raw.label_text,
"Beam length fails the approved scale-aware threshold.");
}
const Vector3 tangent = delta_scaled / length_ratio;
const Vector3 guide{section.first_axis};
const double global_guide_scale = std::max(1.0, maximum_absolute(guide));
const Vector3 guide_scaled = guide / global_guide_scale;
const double projection = guide_scaled.Dot(tangent);
const Vector3 perpendicular = guide_scaled - projection * tangent;
const double guide_norm_ratio =
std::max(1.0 / global_guide_scale, guide_scaled.Norm());
if (!(perpendicular.Norm() > 1.0e-12 * guide_norm_ratio)) {
return ModelFailure(
"invalid-beam-guide-vector", section.location, "BEAM GENERAL SECTION",
raw.label_text,
"The first section axis cannot be zero or tangent-parallel.");
}
return true;
}
void ExpandInstances() {
for (const auto& raw_instance : instances_) {
const RawPart* part = FindPart(raw_instance.part_name);
if (part == nullptr) {
InputFailure("unresolved-reference", raw_instance.location, "INSTANCE",
raw_instance.name,
"INSTANCE PART must resolve case-insensitively.");
return;
}
InstanceDefinition instance{
raw_instance.name, part->name, {}, {}, raw_instance.location};
std::map<std::int64_t, EntityIndex> node_indices;
std::map<std::int64_t, EntityIndex> element_indices;
// Instance order, followed by part-local declaration order, is the
// sole source of stable expanded internal IDs.
for (const auto& raw_node : part->nodes) {
if (definition_.nodes.size() >
std::numeric_limits<EntityIndex>::max()) {
InputFailure("entity-index-overflow", raw_node.location, "NODE",
raw_node.label_text,
"Expanded node count exceeds EntityIndex capacity.");
return;
}
const EntityIndex index =
static_cast<EntityIndex>(definition_.nodes.size());
definition_.nodes.push_back(
{{raw_instance.name, raw_node.label, raw_node.label_text},
raw_node.coordinates,
raw_node.location});
node_indices.emplace(raw_node.label, index);
instance.node_mappings.push_back({raw_node.label, index});
}
for (const auto& raw_element : part->elements) {
EntityIndex index = 0U;
if (raw_element.type == RawElement::Type::kB33) {
const auto first = node_indices.find(raw_element.node_labels[0]);
const auto second = node_indices.find(raw_element.node_labels[1]);
const auto& assignments = part_section_assignments_[PartIndex(*part)];
const auto assignment = assignments.find(raw_element.label);
if (first == node_indices.end() || second == node_indices.end() ||
assignment == assignments.end()) {
InputFailure(
"unresolved-reference", raw_element.location, "ELEMENT",
raw_element.label_text,
"Expanded connectivity and section assignment must resolve.");
return;
}
if (definition_.elements.size() >
std::numeric_limits<EntityIndex>::max()) {
InputFailure(
"entity-index-overflow", raw_element.location, "ELEMENT",
raw_element.label_text,
"Expanded element count exceeds EntityIndex capacity.");
return;
}
index = static_cast<EntityIndex>(definition_.elements.size());
const auto& section = definition_.sections[assignment->second.first];
if (!ValidateGeometry(raw_element, definition_.nodes[first->second],
definition_.nodes[second->second], section)) {
return;
}
definition_.elements.push_back(
{{raw_instance.name, raw_element.label, raw_element.label_text},
{first->second, second->second},
assignment->second.second,
assignment->second.first,
raw_element.location});
} else {
std::array<EntityIndex, 4> connected_nodes{};
for (std::size_t node = 0U; node < connected_nodes.size(); ++node) {
const auto found = node_indices.find(raw_element.node_labels[node]);
if (found == node_indices.end()) {
InputFailure("invalid-shell-connectivity", raw_element.location,
"ELEMENT", raw_element.label_text,
"Expanded shell connectivity must resolve.");
return;
}
connected_nodes[node] = found->second;
}
const auto& assignments =
part_shell_section_assignments_[PartIndex(*part)];
const auto assignment = assignments.find(raw_element.label);
if (assignment == assignments.end()) {
InputFailure(
"invalid-shell-section-assignment", raw_element.location,
"ELEMENT", raw_element.label_text,
"Expanded shell section assignment must resolve exactly once.");
return;
}
if (definition_.shell_elements.size() >
std::numeric_limits<EntityIndex>::max()) {
InputFailure(
"entity-index-overflow", raw_element.location, "ELEMENT",
raw_element.label_text,
"Expanded shell element count exceeds EntityIndex capacity.");
return;
}
index = static_cast<EntityIndex>(definition_.shell_elements.size());
definition_.shell_elements.push_back(
{{raw_instance.name, raw_element.label, raw_element.label_text},
raw_element.type == RawElement::Type::kS4
? ShellSourceElementType::kS4
: ShellSourceElementType::kS4r,
connected_nodes,
assignment->second.second,
assignment->second.first,
raw_element.location});
}
element_indices.emplace(raw_element.label, index);
instance.element_mappings.push_back({raw_element.label, index});
}
for (const auto& raw_set : part->node_sets) {
NodeSet set{raw_set.name, raw_instance.name, {}, raw_set.location};
for (const auto member : raw_set.members) {
const auto found = node_indices.find(member);
if (found == node_indices.end()) {
InputFailure("unresolved-reference", raw_set.location, "NSET",
raw_set.name, "Part node-set expansion failed.");
return;
}
set.node_indices.push_back(found->second);
}
definition_.node_sets.push_back(std::move(set));
}
for (const auto& raw_set : part->element_sets) {
ElementSet set{raw_set.name, raw_instance.name, {}, raw_set.location};
for (const auto member : raw_set.members) {
const auto found = element_indices.find(member);
if (found == element_indices.end()) {
InputFailure("unresolved-reference", raw_set.location, "ELSET",
raw_set.name, "Part element-set expansion failed.");
return;
}
set.element_indices.push_back(found->second);
}
definition_.element_sets.push_back(std::move(set));
}
definition_.instances.push_back(std::move(instance));
}
}
void ExpandAssemblySets() {
for (const auto& raw_set : assembly_sets_) {
const RawInstance* raw_instance = FindInstance(raw_set.instance_name);
if (raw_instance == nullptr) {
InputFailure("unresolved-reference", raw_set.location,
raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name,
"Assembly set INSTANCE must resolve.");
return;
}
const auto definition_instance = std::find_if(
definition_.instances.begin(), definition_.instances.end(),
[&raw_instance](const InstanceDefinition& instance) {
return AsciiCaseInsensitiveEquals(instance.name,
raw_instance->name);
});
if (definition_instance == definition_.instances.end()) {
InputFailure("unresolved-reference", raw_set.location,
raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name,
"Assembly set instance expansion is unavailable.");
return;
}
if (raw_set.is_node_set) {
definition_.node_sets.erase(
std::remove_if(
definition_.node_sets.begin(), definition_.node_sets.end(),
[&raw_set](const NodeSet& set) {
return AsciiCaseInsensitiveEquals(set.name, raw_set.name);
}),
definition_.node_sets.end());
NodeSet set{
raw_set.name, definition_instance->name, {}, raw_set.location};
for (const auto member : raw_set.members) {
const auto mapping =
std::find_if(definition_instance->node_mappings.begin(),
definition_instance->node_mappings.end(),
[member](const SourceIndexMapping& value) {
return value.source_label == member;
});
if (mapping == definition_instance->node_mappings.end()) {
InputFailure("unresolved-reference", raw_set.location, "NSET",
raw_set.name,
"Assembly node-set member must resolve in its "
"instance.");
return;
}
set.node_indices.push_back(mapping->internal_index);
}
definition_.node_sets.push_back(std::move(set));
} else {
definition_.element_sets.erase(
std::remove_if(definition_.element_sets.begin(),
definition_.element_sets.end(),
[&raw_set](const ElementSet& set) {
return AsciiCaseInsensitiveEquals(set.name,
raw_set.name);
}),
definition_.element_sets.end());
ElementSet set{
raw_set.name, definition_instance->name, {}, raw_set.location};
for (const auto member : raw_set.members) {
const auto mapping =
std::find_if(definition_instance->element_mappings.begin(),
definition_instance->element_mappings.end(),
[member](const SourceIndexMapping& value) {
return value.source_label == member;
});
if (mapping == definition_instance->element_mappings.end()) {
InputFailure("unresolved-reference", raw_set.location, "ELSET",
raw_set.name,
"Assembly element-set member must resolve in "
"its instance.");
return;
}
set.element_indices.push_back(mapping->internal_index);
}
definition_.element_sets.push_back(std::move(set));
}
}
}
/// @brief Builds one stable node-target index from the completed candidate.
SourceTargetIndex BuildSourceTargetIndex() const {
std::vector<SourceTargetIndexEntry> entries;
std::size_t declaration_order = 0U;
for (std::size_t node = 0U; node < definition_.nodes.size(); ++node) {
const auto& source_id = definition_.nodes[node].source_id;
entries.push_back({SourceEntityKind::kNode, source_id.instance_name, "",
source_id, static_cast<EntityIndex>(node),
declaration_order++});
}
for (const auto& set : definition_.node_sets) {
for (const EntityIndex node : set.node_indices) {
const auto& source_id = definition_.nodes[node].source_id;
entries.push_back({SourceEntityKind::kNode,
set.instance_name.value_or(source_id.instance_name),
set.name, source_id, node, declaration_order++});
}
}
return SourceTargetIndex{std::move(entries)};
}
std::optional<std::vector<EntityIndex>> ResolveNodeTarget(
const SourceTargetResolver& resolver, const std::string& target,
const SourceLocation& location, const std::string& keyword) {
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", target});
if (!resolved.HasValue()) {
InputFailure(
"unresolved-reference", location, keyword, target,
"The boundary or load target must resolve unambiguously to one "
"node or one node set.");
return std::nullopt;
}
std::vector<EntityIndex> indices;
indices.reserve(resolved.Value().size());
for (const auto& entry : resolved.Value()) {
indices.push_back(entry.entity_index);
}
return indices;
}
void FinalizeStep() {
std::vector<BoundaryCondition> boundaries = model_boundaries_;
boundaries.insert(boundaries.end(), step_.boundaries.begin(),
step_.boundaries.end());
const SourceTargetIndex target_index = BuildSourceTargetIndex();
const SourceTargetResolver target_resolver{target_index};
std::map<std::pair<EntityIndex, int>, double> prescribed_values;
for (const auto& boundary : boundaries) {
auto target = ResolveNodeTarget(target_resolver, boundary.target,
boundary.location, "BOUNDARY");
if (!target) {
return;
}
for (const auto node : *target) {
for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) {
const auto key = std::make_pair(node, dof);
const auto existing = prescribed_values.find(key);
if (existing != prescribed_values.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;
}
prescribed_values[key] = boundary.value;
}
}
}
for (const auto& load : step_.loads) {
if (!ResolveNodeTarget(target_resolver, 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_.static_values[0], step_.static_values[1],
step_.static_values[2], step_.static_values[3],
step_.location});
}
const ParsedInput& input_;
ModelDefinition definition_{};
std::optional<MappingFailure> failure_;
std::vector<RawPart> parts_;
std::vector<RawInstance> instances_;
std::vector<RawAssemblySet> assembly_sets_;
std::vector<RawMaterial> materials_;
std::vector<BoundaryCondition> model_boundaries_;
RawStep step_{};
std::vector<std::map<std::int64_t, std::pair<EntityIndex, EntityIndex>>>
part_section_assignments_;
std::vector<std::map<std::int64_t, std::pair<EntityIndex, EntityIndex>>>
part_shell_section_assignments_;
std::optional<std::size_t> current_part_;
std::optional<std::size_t> pending_section_;
std::optional<std::size_t> material_eligible_;
std::optional<ElementFamily> model_element_family_;
bool heading_seen_{false};
bool part_elements_seen_{false};
bool part_sets_seen_{false};
bool part_sections_seen_{false};
bool beam_section_context_active_{false};
bool assembly_seen_{false};
bool assembly_set_seen_{false};
bool model_boundary_seen_{false};
bool in_assembly_{false};
bool in_instance_{false};
bool step_seen_{false};
bool in_step_{false};
bool step_load_seen_{false};
bool step_no_op_seen_{false};
bool active_output_{false};
};
} // namespace
Result<Domain> AbaqusDomainMapper::Map(const ParsedInput& input) const {
return MappingContext{input}.Run();
}
} // namespace fesa