#include #include #include #include #include #include #include #include #include #include #include #include namespace fesa { namespace { using LabelMap = std::map; using ElementLabelMap = std::map; using RawSetMap = std::map, std::less<>>; const std::string* parameter( const DeckRecord& record, const std::string_view name) { const auto found = record.parameters.find(name); return found == record.parameters.end() ? nullptr : &found->second; } bool is_mesh_record(const DeckRecord& record) { return record.keyword == "NODE" || record.keyword == "ELEMENT"; } class DeckMapper final { public: explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {} [[nodiscard]] DomainBuildResult map() { if (!select_active_scope()) { return failure(); } collect_materials(); collect_nodes(); collect_raw_sets(*active_records_, false); if (active_assembly_ != nullptr) { collect_raw_sets(active_assembly_->records, true); } collect_sections(); collect_elements(); emit_sets(); collect_step(); if (!diagnostics_.empty()) { return failure(); } return std::move(builder_).build(); } private: struct SectionAssignment final { MaterialId material; SectionId section; }; [[nodiscard]] DomainBuildResult failure() { return {std::nullopt, std::move(diagnostics_)}; } void add_error( std::string code, std::string message, const SourceLocation& source) { diagnostics_.push_back({ DiagnosticStage::semantic, Severity::error, std::move(code), std::move(message), source, }); } const std::string* require_parameter( const DeckRecord& record, const std::string_view name) { const std::string* value = parameter(record, name); if (value == nullptr || value->empty()) { add_error( "abaqus.semantic.missing_parameter", "*" + record.keyword + " requires parameter " + std::string{name} + ".", record.source); return nullptr; } return value; } std::optional parse_label( const std::string_view text, const DeckRecord& record, const std::string_view purpose) { std::int64_t value = 0; const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value); if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size() || value <= 0) { add_error( "abaqus.semantic.invalid_label", "Invalid " + std::string{purpose} + " label '" + std::string{text} + "'.", record.source); return std::nullopt; } return value; } std::optional parse_dof( const std::string_view text, const DeckRecord& record) { const std::optional value = parse_label(text, record, "degree-of-freedom"); if (!value.has_value()) { return std::nullopt; } if (*value > 6) { add_error( "abaqus.semantic.invalid_dof", "Phase 1 supports only degrees of freedom 1 through 6.", record.source); return std::nullopt; } return static_cast(*value); } std::optional parse_real( const std::string_view text, const DeckRecord& record, const std::string_view purpose) { double value = 0.0; const auto parsed = std::from_chars( text.data(), text.data() + text.size(), value, std::chars_format::general); if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size()) { add_error( "abaqus.semantic.invalid_number", "Invalid " + std::string{purpose} + " value '" + std::string{text} + "'.", record.source); return std::nullopt; } return value; } bool select_active_scope() { const bool has_hierarchical_input = !deck_.parts.empty() || deck_.assembly.has_value(); if (!has_hierarchical_input) { active_records_ = &deck_.global_records; return true; } if (std::ranges::any_of(deck_.global_records, is_mesh_record)) { add_error( "abaqus.semantic.mixed_mesh_organization", "Flat mesh records cannot be mixed with Part/Assembly input.", std::ranges::find_if( deck_.global_records, is_mesh_record)->source); return false; } if (!deck_.assembly.has_value()) { const SourceLocation source = deck_.parts.empty() ? SourceLocation{} : deck_.parts[0].source; add_error( "abaqus.semantic.assembly_count", "Hierarchical Phase 1 input requires exactly one Assembly.", source); return false; } active_assembly_ = &*deck_.assembly; if (active_assembly_->instances.size() != 1U) { const SourceLocation& source = active_assembly_->instances.size() > 1U ? active_assembly_->instances[1].source : active_assembly_->source; add_error( "abaqus.semantic.instance_count", "Phase 1 requires exactly one Instance.", source); return false; } const ParsedInstance& instance = active_assembly_->instances.front(); if (!instance.transform_data.empty()) { add_error( "abaqus.semantic.instance_transform", "Instance translation and rotation data are unsupported.", instance.source); return false; } const auto part = std::ranges::find( deck_.parts, instance.part_name, &ParsedPart::name); if (part == deck_.parts.end()) { add_error( "abaqus.semantic.missing_part", "Instance '" + instance.name + "' references missing Part '" + instance.part_name + "'.", instance.source); return false; } active_records_ = &part->records; part_name_ = part->name; instance_name_ = instance.name; return true; } void collect_materials() { const std::string* current_name = nullptr; for (const DeckRecord& record : deck_.global_records) { if (record.keyword == "MATERIAL") { current_name = require_parameter(record, "NAME"); continue; } if (record.keyword != "ELASTIC") { continue; } if (current_name == nullptr) { add_error( "abaqus.semantic.elastic_without_material", "*ELASTIC requires a preceding *MATERIAL.", record.source); continue; } if (record.data.empty() || record.data[0].size() < 2U) { add_error( "abaqus.semantic.invalid_elastic_data", "*ELASTIC requires Young's modulus and Poisson ratio.", record.source); continue; } const auto young = parse_real(record.data[0][0], record, "Young's modulus"); const auto poisson = parse_real(record.data[0][1], record, "Poisson ratio"); if (!young.has_value() || !poisson.has_value()) { continue; } const MaterialId id{next_material_id_++}; if (!material_ids_.emplace(*current_name, id).second) { add_error( "abaqus.semantic.duplicate_material", "Material '" + *current_name + "' is defined more than once.", record.source); continue; } builder_.add_material({id, *current_name, *young, *poisson}); } } void collect_nodes() { for (const DeckRecord& record : *active_records_) { if (record.keyword != "NODE") { continue; } for (const auto& row : record.data) { if (row.size() < 4U) { add_error( "abaqus.semantic.invalid_node_data", "*NODE requires a label and three coordinates.", record.source); continue; } const auto label = parse_label(row[0], record, "node"); const auto x = parse_real(row[1], record, "node coordinate"); const auto y = parse_real(row[2], record, "node coordinate"); const auto z = parse_real(row[3], record, "node coordinate"); if (!label.has_value() || !x.has_value() || !y.has_value() || !z.has_value()) { continue; } const NodeId id{next_node_id_++}; node_ids_.emplace(*label, id); builder_.add_node({ id, EntityOrigin{part_name_, instance_name_, *label}, Vec3{*x, *y, *z}, }); } } } void collect_raw_sets( const std::vector& records, const bool assembly_scope) { for (const DeckRecord& record : records) { const bool is_node_set = record.keyword == "NSET"; const bool is_element_set = record.keyword == "ELSET"; if (!is_node_set && !is_element_set) { continue; } const std::string* name = require_parameter(record, is_node_set ? "NSET" : "ELSET"); if (name == nullptr) { continue; } if (assembly_scope) { const std::string* instance = parameter(record, "INSTANCE"); if (instance == nullptr || *instance != instance_name_) { add_error( "abaqus.semantic.wrong_instance", "Assembly set '" + *name + "' must reference the active Instance.", record.source); continue; } } std::vector labels; for (const auto& row : record.data) { for (const std::string& field : row) { if (field.empty()) { continue; } const auto label = parse_label(field, record, "set member"); if (label.has_value()) { labels.push_back(*label); } } } RawSetMap& sets = is_node_set ? raw_node_sets_ : raw_element_sets_; sets[*name] = std::move(labels); } } void collect_sections() { for (const DeckRecord& record : *active_records_) { if (record.keyword != "BEAM GENERAL SECTION") { continue; } const std::string* element_set = require_parameter(record, "ELSET"); const std::string* material_name = require_parameter(record, "MATERIAL"); if (element_set == nullptr || material_name == nullptr) { continue; } const auto material = material_ids_.find(*material_name); if (material == material_ids_.end()) { add_error( "abaqus.semantic.missing_material", "Beam section references missing Material '" + *material_name + "'.", record.source); continue; } if (record.data.size() < 2U || record.data[0].size() < 5U || record.data[1].size() < 3U) { add_error( "abaqus.semantic.invalid_section_data", "*BEAM GENERAL SECTION requires general properties and " "an orientation vector.", record.source); continue; } const auto area = parse_real(record.data[0][0], record, "area"); const auto iy = parse_real(record.data[0][1], record, "Iy"); const auto iz = parse_real(record.data[0][3], record, "Iz"); const auto torsion = parse_real(record.data[0][4], record, "torsion J"); const auto ox = parse_real(record.data[1][0], record, "orientation"); const auto oy = parse_real(record.data[1][1], record, "orientation"); const auto oz = parse_real(record.data[1][2], record, "orientation"); if (!area.has_value() || !iy.has_value() || !iz.has_value() || !torsion.has_value() || !ox.has_value() || !oy.has_value() || !oz.has_value()) { continue; } const SectionId section_id{next_section_id_++}; if (!section_assignments_ .emplace( *element_set, SectionAssignment{material->second, section_id}) .second) { add_error( "abaqus.semantic.duplicate_section_assignment", "Element set '" + *element_set + "' has more than one section assignment.", record.source); continue; } builder_.add_section({ section_id, *element_set, *area, *iy, *iz, *torsion, 5.0 * *area / 6.0, 5.0 * *area / 6.0, ShearPropertySource::phase1_default, Vec3{*ox, *oy, *oz}, {}, }); } } void collect_elements() { for (const DeckRecord& record : *active_records_) { if (record.keyword != "ELEMENT") { continue; } const std::string* type = require_parameter(record, "TYPE"); const std::string* element_set = require_parameter(record, "ELSET"); if (type == nullptr || element_set == nullptr) { continue; } if (*type != "B31") { add_error( "abaqus.semantic.unsupported_element", "Phase 1 supports only B31 elements.", record.source); continue; } const auto assignment = section_assignments_.find(*element_set); if (assignment == section_assignments_.end()) { add_error( "abaqus.semantic.missing_section", "Element set '" + *element_set + "' has no Beam section assignment.", record.source); continue; } for (const auto& row : record.data) { if (row.size() < 3U) { add_error( "abaqus.semantic.invalid_element_data", "B31 element data requires a label and two nodes.", record.source); continue; } const auto label = parse_label(row[0], record, "element"); const auto first_label = parse_label(row[1], record, "node"); const auto second_label = parse_label(row[2], record, "node"); if (!label.has_value() || !first_label.has_value() || !second_label.has_value()) { continue; } const auto first = node_ids_.find(*first_label); const auto second = node_ids_.find(*second_label); if (first == node_ids_.end() || second == node_ids_.end()) { add_error( "abaqus.semantic.missing_node", "B31 element references a missing node label.", record.source); continue; } const ElementId id{next_element_id_++}; element_ids_.emplace(*label, id); builder_.add_beam_element({ id, EntityOrigin{part_name_, instance_name_, *label}, {first->second, second->second}, assignment->second.material, assignment->second.section, }); } } } template std::vector resolve_set( const std::vector& labels, const Lookup& lookup, const std::string& name) { std::vector members; for (const std::int64_t label : labels) { const auto found = lookup.find(label); if (found == lookup.end()) { diagnostics_.push_back({ DiagnosticStage::semantic, Severity::error, "abaqus.semantic.missing_set_member", "Set '" + name + "' references a missing entity label " + std::to_string(label) + ".", std::nullopt, }); continue; } members.push_back(found->second); } std::ranges::sort(members); members.erase(std::ranges::unique(members).begin(), members.end()); return members; } void emit_sets() { for (const auto& [name, labels] : raw_node_sets_) { std::vector members = resolve_set(labels, node_ids_, name); node_sets_[name] = members; builder_.add_node_set({name, std::move(members)}); } for (const auto& [name, labels] : raw_element_sets_) { std::vector members = resolve_set(labels, element_ids_, name); builder_.add_element_set({name, std::move(members)}); } } std::vector resolve_node_target( const std::string& target, const DeckRecord& record) { std::int64_t label = 0; const auto parsed = std::from_chars(target.data(), target.data() + target.size(), label); if (parsed.ec == std::errc{} && parsed.ptr == target.data() + target.size()) { const auto found = node_ids_.find(label); if (found != node_ids_.end()) { return {found->second}; } } else { const auto found = node_sets_.find(target); if (found != node_sets_.end()) { return found->second; } } add_error( "abaqus.semantic.missing_node_target", "Node target '" + target + "' does not resolve.", record.source); return {}; } void collect_boundary( const DeckRecord& record, StepDefinition& step) { for (const auto& row : record.data) { if (row.size() < 2U) { add_error( "abaqus.semantic.invalid_boundary_data", "*BOUNDARY requires a target and degree of freedom.", record.source); continue; } const auto first_dof = parse_dof(row[1], record); const auto last_dof = row.size() >= 3U && !row[2].empty() ? parse_dof(row[2], record) : first_dof; const auto value = row.size() >= 4U && !row[3].empty() ? parse_real( row[3], record, "prescribed value") : std::optional{0.0}; if (!first_dof.has_value() || !last_dof.has_value() || !value.has_value()) { continue; } if (*last_dof < *first_dof) { add_error( "abaqus.semantic.invalid_dof_range", "*BOUNDARY degree-of-freedom range is reversed.", record.source); continue; } const std::vector nodes = resolve_node_target(row[0], record); for (const NodeId node : nodes) { for (std::uint8_t dof = *first_dof; dof <= *last_dof; ++dof) { step.prescribed_dofs.push_back({node, dof, *value}); } } } } void collect_loads( const DeckRecord& record, std::map>& loads) { for (const auto& row : record.data) { if (row.size() < 3U) { add_error( "abaqus.semantic.invalid_cload_data", "*CLOAD requires a target, degree of freedom, and value.", record.source); continue; } const auto dof = parse_dof(row[1], record); const auto value = parse_real(row[2], record, "concentrated load"); if (!dof.has_value() || !value.has_value()) { continue; } const std::vector nodes = resolve_node_target(row[0], record); for (const NodeId node : nodes) { loads[node.value()][static_cast(*dof - 1U)] += *value; } } } void collect_step() { StepDefinition step; std::map> loads; bool has_step = false; for (const DeckRecord& record : deck_.global_records) { if (record.keyword == "STEP") { has_step = true; const std::string* name = parameter(record, "NAME"); step.name = name == nullptr ? "Step-1" : *name; } else if (record.keyword == "BOUNDARY") { collect_boundary(record, step); } else if (record.keyword == "CLOAD") { collect_loads(record, loads); } } if (!has_step) { const SourceLocation source = deck_.global_records.empty() ? SourceLocation{} : deck_.global_records.front().source; add_error( "abaqus.semantic.missing_step", "Phase 1 input requires one *STEP.", source); return; } for (const auto& [node, values] : loads) { step.nodal_loads.push_back({NodeId{node}, values}); } builder_.set_step(std::move(step)); } const ParsedDeck& deck_; DomainBuilder builder_; std::vector diagnostics_; const std::vector* active_records_ = nullptr; const ParsedAssembly* active_assembly_ = nullptr; std::string part_name_; std::string instance_name_; LabelMap node_ids_; ElementLabelMap element_ids_; std::map> material_ids_; std::map> section_assignments_; RawSetMap raw_node_sets_; RawSetMap raw_element_sets_; std::map, std::less<>> node_sets_; std::int64_t next_node_id_ = 0; std::int64_t next_element_id_ = 0; std::int64_t next_material_id_ = 0; std::int64_t next_section_id_ = 0; }; } // namespace DomainBuildResult map_deck_to_domain(const ParsedDeck& deck) { return DeckMapper{deck}.map(); } } // namespace fesa