feat(domain-and-input-skeleton): step 3 — active-instance-domain-normalization

This commit is contained in:
KOKO\Mimi
2026-07-30 18:10:52 +09:00
parent 0f53ec48eb
commit 154f8ef4af
5 changed files with 987 additions and 0 deletions
+684
View File
@@ -0,0 +1,684 @@
#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) {
add_error(
"abaqus.semantic.instance_count",
"Phase 1 requires exactly one Instance.",
active_assembly_->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