689 lines
24 KiB
C++
689 lines
24 KiB
C++
#include <fesa/io/abaqus/semantic_mapper.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <charconv>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace fesa {
|
|
namespace {
|
|
|
|
using LabelMap = std::map<std::int64_t, NodeId>;
|
|
using ElementLabelMap = std::map<std::int64_t, ElementId>;
|
|
using RawSetMap = std::map<std::string, std::vector<std::int64_t>, 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<std::int64_t> 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<std::uint8_t> parse_dof(
|
|
const std::string_view text,
|
|
const DeckRecord& record) {
|
|
const std::optional<std::int64_t> 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<std::uint8_t>(*value);
|
|
}
|
|
|
|
std::optional<double> 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<DeckRecord>& 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<std::int64_t> 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 <class Id, class Lookup>
|
|
std::vector<Id> resolve_set(
|
|
const std::vector<std::int64_t>& labels,
|
|
const Lookup& lookup,
|
|
const std::string& name) {
|
|
std::vector<Id> 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<NodeId> members =
|
|
resolve_set<NodeId>(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<ElementId> members =
|
|
resolve_set<ElementId>(labels, element_ids_, name);
|
|
builder_.add_element_set({name, std::move(members)});
|
|
}
|
|
}
|
|
|
|
std::vector<NodeId> 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<double>{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<NodeId> 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<std::int64_t, std::array<double, 6>>& 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<NodeId> nodes =
|
|
resolve_node_target(row[0], record);
|
|
for (const NodeId node : nodes) {
|
|
loads[node.value()][static_cast<std::size_t>(*dof - 1U)] +=
|
|
*value;
|
|
}
|
|
}
|
|
}
|
|
|
|
void collect_step() {
|
|
StepDefinition step;
|
|
std::map<std::int64_t, std::array<double, 6>> 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<Diagnostic> diagnostics_;
|
|
const std::vector<DeckRecord>* active_records_ = nullptr;
|
|
const ParsedAssembly* active_assembly_ = nullptr;
|
|
std::string part_name_;
|
|
std::string instance_name_;
|
|
LabelMap node_ids_;
|
|
ElementLabelMap element_ids_;
|
|
std::map<std::string, MaterialId, std::less<>> material_ids_;
|
|
std::map<std::string, SectionAssignment, std::less<>>
|
|
section_assignments_;
|
|
RawSetMap raw_node_sets_;
|
|
RawSetMap raw_element_sets_;
|
|
std::map<std::string, std::vector<NodeId>, 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
|