feat(abaqus-subset-completion): step 3 — material-section-and-shear-defaults

This commit is contained in:
KOKO\Mimi
2026-08-01 02:59:40 +09:00
parent 8cd2d5b2ae
commit a1fed69c47
5 changed files with 727 additions and 128 deletions
+368 -126
View File
@@ -1,14 +1,19 @@
#include <fesa/io/abaqus/semantic_mapper.hpp>
#include <fesa/io/abaqus/active_input.hpp>
#include <fesa/io/abaqus/set_resolver.hpp>
#include <algorithm>
#include <array>
#include <charconv>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <map>
#include <optional>
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <utility>
@@ -28,6 +33,24 @@ const std::string* parameter(
return found == record.parameters.end() ? nullptr : &found->second;
}
SourceLocation data_source(
const DeckRecord& record,
const std::size_t row) {
return row < record.data_sources.size()
? record.data_sources[row]
: record.source;
}
bool parse_number(const std::string_view text, double& value) {
const auto parsed = std::from_chars(
text.data(),
text.data() + text.size(),
value,
std::chars_format::general);
return parsed.ec == std::errc{} &&
parsed.ptr == text.data() + text.size() && std::isfinite(value);
}
class DeckMapper final {
public:
explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}
@@ -39,15 +62,20 @@ public:
return failure();
}
active_records_ = selected.input->part_records;
assembly_records_ = selected.input->assembly_records;
flat_ = selected.input->flat;
part_name_ = std::move(selected.input->part_name);
instance_name_ = std::move(selected.input->instance_name);
collect_materials();
collect_nodes();
collect_raw_sets(active_records_, false);
if (!assembly_records_.empty()) {
collect_raw_sets(assembly_records_, true);
SetResolutionResult resolved_sets = resolve_sets(deck_);
if (!resolved_sets.diagnostics.empty()) {
diagnostics_.insert(
diagnostics_.end(),
std::make_move_iterator(resolved_sets.diagnostics.begin()),
std::make_move_iterator(resolved_sets.diagnostics.end()));
} else {
collect_resolved_sets(resolved_sets.sets);
}
collect_sections();
collect_elements();
@@ -66,6 +94,19 @@ private:
SectionId section;
};
struct MaterialProperties final {
MaterialId id;
double young;
double poisson;
};
struct MaterialDefinition final {
std::string name;
SourceLocation source;
const DeckRecord* elastic = nullptr;
bool duplicate = false;
};
[[nodiscard]] DomainBuildResult failure() {
return {std::nullopt, std::move(diagnostics_)};
}
@@ -158,47 +199,105 @@ private:
}
void collect_materials() {
const std::string* current_name = nullptr;
std::vector<MaterialDefinition> definitions;
std::map<std::string, std::size_t, std::less<>> first_definition;
std::optional<std::size_t> current_definition;
for (const DeckRecord& record : deck_.global_records) {
if (record.keyword == "MATERIAL") {
current_name = require_parameter(record, "NAME");
const std::string* name = require_parameter(record, "NAME");
if (name == nullptr) {
current_definition.reset();
continue;
}
const bool duplicate = first_definition.contains(*name);
if (duplicate) {
add_error(
"abaqus.semantic.duplicate_material",
"Material '" + *name +
"' is defined more than once.",
record.source);
}
definitions.push_back({
*name,
record.source,
nullptr,
duplicate,
});
current_definition = definitions.size() - 1U;
first_definition.try_emplace(*name, *current_definition);
continue;
}
if (record.keyword != "ELASTIC") {
continue;
}
if (current_name == nullptr) {
if (!current_definition.has_value()) {
add_error(
"abaqus.semantic.elastic_without_material",
"*ELASTIC requires a preceding *MATERIAL.",
record.source);
continue;
}
if (record.data.empty() || record.data[0].size() < 2U) {
MaterialDefinition& definition =
definitions[*current_definition];
if (definition.elastic != nullptr) {
add_error(
"abaqus.semantic.invalid_elastic_data",
"*ELASTIC requires Young's modulus and Poisson ratio.",
"A material accepts exactly one *ELASTIC record.",
record.source);
continue;
}
definition.elastic = &record;
}
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()) {
for (const MaterialDefinition& definition : definitions) {
if (definition.duplicate) {
continue;
}
if (definition.elastic == nullptr) {
add_error(
"abaqus.semantic.missing_elastic",
"Material '" + definition.name +
"' requires one *ELASTIC record.",
definition.source);
continue;
}
const DeckRecord& elastic = *definition.elastic;
const SourceLocation source =
elastic.data.empty() ? elastic.source
: data_source(elastic, 0U);
if (elastic.data.size() != 1U ||
elastic.data[0].size() != 2U ||
elastic.data[0][0].empty() ||
elastic.data[0][1].empty()) {
add_error(
"abaqus.semantic.invalid_elastic_data",
"*ELASTIC requires exactly one E, nu data row.",
source);
continue;
}
double young = 0.0;
double poisson = 0.0;
if (!parse_number(elastic.data[0][0], young) ||
!parse_number(elastic.data[0][1], poisson) ||
young <= 0.0 || poisson <= -1.0 || poisson >= 0.5) {
add_error(
"abaqus.semantic.invalid_elastic_data",
"*ELASTIC requires finite E > 0 and -1 < nu < 0.5.",
source);
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});
materials_.emplace(
definition.name,
MaterialProperties{id, young, poisson});
builder_.add_material(
{id, definition.name, young, poisson});
}
}
@@ -236,125 +335,267 @@ private:
}
}
void collect_raw_sets(
const std::span<const 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) {
void collect_resolved_sets(const std::vector<ResolvedSet>& sets) {
for (const ResolvedSet& set : sets) {
const bool mesh_scope =
(flat_ && set.scope == ResolvedSetScope::global) ||
(!flat_ && set.scope == ResolvedSetScope::part &&
set.scope_name == part_name_);
const bool assembly_scope =
!flat_ && set.scope == ResolvedSetScope::assembly &&
deck_.assembly.has_value() &&
set.scope_name == deck_.assembly->name;
if (!mesh_scope && !assembly_scope) {
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;
}
}
RawSetMap& target = set.kind == ResolvedSetKind::node
? raw_node_sets_
: raw_element_sets_;
std::vector<std::int64_t>& labels = target[set.set_name];
labels.insert(
labels.end(),
set.sorted_unique_labels.begin(),
set.sorted_unique_labels.end());
std::ranges::sort(labels);
labels.erase(std::ranges::unique(labels).begin(), labels.end());
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);
}
}
if (mesh_scope && set.kind == ResolvedSetKind::element) {
active_element_sets_[set.set_name] =
set.sorted_unique_labels;
}
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_) {
for (std::size_t index = 0; index < active_records_.size(); ++index) {
const DeckRecord& record = active_records_[index];
if (record.keyword == "TRANSVERSE SHEAR STIFFNESS") {
add_error(
"abaqus.semantic.orphan_transverse_shear",
"*TRANSVERSE SHEAR STIFFNESS must immediately follow "
"*BEAM GENERAL SECTION.",
record.source);
continue;
}
if (record.keyword != "BEAM GENERAL SECTION") {
continue;
}
const DeckRecord* shear = nullptr;
if (index + 1U < active_records_.size() &&
active_records_[index + 1U].keyword ==
"TRANSVERSE SHEAR STIFFNESS") {
shear = &active_records_[index + 1U];
++index;
}
bool valid = true;
const std::string* section_type =
require_parameter(record, "SECTION");
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()) {
if (section_type == nullptr || element_set == nullptr ||
material_name == nullptr) {
valid = false;
} else if (*section_type != "GENERAL") {
add_error(
"abaqus.semantic.missing_material",
"Beam section references missing Material '" +
*material_name + "'.",
"abaqus.semantic.unsupported_section",
"Phase 1 supports only SECTION=GENERAL.",
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;
valid = false;
}
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) {
if (element_set != nullptr &&
!assigned_element_set_names_.insert(*element_set).second) {
add_error(
"abaqus.semantic.duplicate_section_assignment",
"Element set '" + *element_set +
"' has more than one section assignment.",
record.source);
valid = false;
}
auto material = materials_.end();
if (material_name != nullptr) {
material = materials_.find(*material_name);
if (material == materials_.end()) {
add_error(
"abaqus.semantic.missing_material",
"Beam section references missing Material '" +
*material_name + "'.",
record.source);
valid = false;
}
}
auto resolved_set = active_element_sets_.end();
if (element_set != nullptr) {
resolved_set = active_element_sets_.find(*element_set);
if (resolved_set == active_element_sets_.end()) {
add_error(
"abaqus.semantic.missing_element_set",
"Beam section references missing element set '" +
*element_set + "'.",
record.source);
valid = false;
}
}
std::array<double, 5> properties{};
std::array<double, 3> orientation{};
if (record.data.size() != 2U ||
record.data[0].size() != properties.size() ||
record.data[1].size() != orientation.size()) {
SourceLocation section_data_source = record.source;
if (!record.data.empty()) {
if (record.data[0].size() != properties.size()) {
section_data_source = data_source(record, 0U);
} else if (record.data.size() > 2U) {
section_data_source = data_source(record, 2U);
} else if (record.data.size() == 2U) {
section_data_source = data_source(record, 1U);
}
}
add_error(
"abaqus.semantic.invalid_section_data",
"*BEAM GENERAL SECTION requires exactly A, Iy, Iyz, Iz, "
"J and one three-component orientation row.",
section_data_source);
valid = false;
} else {
bool valid_properties = true;
for (std::size_t field = 0; field < properties.size(); ++field) {
valid_properties = parse_number(
record.data[0][field],
properties[field]) &&
valid_properties;
}
valid_properties =
valid_properties && properties[0] > 0.0 &&
properties[1] > 0.0 && properties[2] == 0.0 &&
properties[3] > 0.0 && properties[4] > 0.0;
if (!valid_properties) {
add_error(
"abaqus.semantic.invalid_section_data",
"General Beam properties require finite positive A, "
"Iy, Iz, J and Iyz=0.",
data_source(record, 0U));
}
bool valid_orientation = true;
for (std::size_t field = 0; field < orientation.size(); ++field) {
valid_orientation = parse_number(
record.data[1][field],
orientation[field]) &&
valid_orientation;
}
if (!valid_orientation) {
add_error(
"abaqus.semantic.invalid_section_data",
"Beam section orientation components must be finite.",
data_source(record, 1U));
}
valid = valid && valid_properties && valid_orientation;
}
double shear_area_y = 0.0;
double shear_area_z = 0.0;
ShearPropertySource shear_source =
ShearPropertySource::phase1_default;
if (shear == nullptr) {
shear_area_y = 5.0 * properties[0] / 6.0;
shear_area_z = shear_area_y;
} else {
const SourceLocation shear_source_location =
shear->data.empty() ? shear->source
: data_source(*shear, 0U);
std::array<double, 3> values{};
bool valid_shear =
shear->data.size() == 1U &&
shear->data[0].size() == values.size();
if (valid_shear) {
for (std::size_t field = 0; field < values.size(); ++field) {
valid_shear = parse_number(
shear->data[0][field], values[field]) &&
valid_shear;
}
}
if (!valid_shear || values[0] <= 0.0 || values[1] <= 0.0) {
add_error(
"abaqus.semantic.invalid_transverse_shear_data",
"*TRANSVERSE SHEAR STIFFNESS requires finite positive "
"K23, K13 and numeric SCF=0.",
shear_source_location);
valid = false;
} else if (values[2] != 0.0) {
add_error(
"abaqus.semantic.nonzero_scf",
"Phase 1 supports only SCF=0.",
shear_source_location);
valid = false;
} else if (material != materials_.end()) {
const double shear_modulus =
material->second.young /
(2.0 * (1.0 + material->second.poisson));
shear_area_y = values[0] / shear_modulus;
shear_area_z = values[1] / shear_modulus;
if (!std::isfinite(shear_area_y) ||
!std::isfinite(shear_area_z) ||
shear_area_y <= 0.0 || shear_area_z <= 0.0) {
add_error(
"abaqus.semantic.invalid_transverse_shear_data",
"Transverse stiffness produces an invalid effective "
"shear area.",
shear_source_location);
valid = false;
}
shear_source = ShearPropertySource::input;
}
}
if (!valid || element_set == nullptr ||
material == materials_.end() ||
resolved_set == active_element_sets_.end()) {
continue;
}
const SectionId section_id{next_section_id_++};
const SectionAssignment assignment{
material->second.id,
section_id,
};
bool duplicate_member = false;
for (const std::int64_t label : resolved_set->second) {
if (element_assignments_.contains(label)) {
duplicate_member = true;
break;
}
}
if (duplicate_member) {
add_error(
"abaqus.semantic.duplicate_section_assignment",
"An element has more than one section assignment.",
record.source);
continue;
}
for (const std::int64_t label : resolved_set->second) {
element_assignments_.emplace(label, assignment);
}
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},
properties[0],
properties[1],
properties[3],
properties[4],
shear_area_y,
shear_area_z,
shear_source,
Vec3{orientation[0], orientation[1], orientation[2]},
{},
});
}
@@ -367,9 +608,7 @@ private:
}
const std::string* type = require_parameter(record, "TYPE");
const std::string* element_set =
require_parameter(record, "ELSET");
if (type == nullptr || element_set == nullptr) {
if (type == nullptr) {
continue;
}
if (*type != "B31") {
@@ -379,15 +618,6 @@ private:
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) {
@@ -407,6 +637,16 @@ private:
continue;
}
const auto assignment = element_assignments_.find(*label);
if (assignment == element_assignments_.end()) {
add_error(
"abaqus.semantic.missing_section",
"B31 element " + std::to_string(*label) +
" has no Beam section assignment.",
record.source);
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()) {
@@ -601,14 +841,16 @@ private:
DomainBuilder builder_;
std::vector<Diagnostic> diagnostics_;
std::span<const DeckRecord> active_records_;
std::span<const DeckRecord> assembly_records_;
bool flat_ = true;
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_;
std::map<std::string, MaterialProperties, std::less<>> materials_;
std::map<std::string, std::vector<std::int64_t>, std::less<>>
active_element_sets_;
std::map<std::int64_t, SectionAssignment> element_assignments_;
std::set<std::string, std::less<>> assigned_element_set_names_;
RawSetMap raw_node_sets_;
RawSetMap raw_element_sets_;
std::map<std::string, std::vector<NodeId>, std::less<>> node_sets_;