fix(input): enforce strict Abaqus subset contract

This commit is contained in:
KOKO\Mimi
2026-08-01 03:48:29 +09:00
parent 9269847c83
commit af886f3f90
25 changed files with 722 additions and 34 deletions
+21 -7
View File
@@ -1,6 +1,7 @@
#include <fesa/io/abaqus/active_input.hpp>
#include <algorithm>
#include <set>
#include <string>
#include <string_view>
#include <utility>
@@ -15,8 +16,11 @@ const std::string* parameter(
return found == record.parameters.end() ? nullptr : &found->second;
}
bool is_mesh_record(const DeckRecord& record) {
return record.keyword == "NODE" || record.keyword == "ELEMENT";
bool is_flat_model_record(const DeckRecord& record) {
return record.keyword == "NODE" || record.keyword == "ELEMENT" ||
record.keyword == "NSET" || record.keyword == "ELSET" ||
record.keyword == "BEAM GENERAL SECTION" ||
record.keyword == "TRANSVERSE SHEAR STIFFNESS";
}
ActiveInputResult failure(
@@ -53,13 +57,23 @@ ActiveInputResult select_active_input(const ParsedDeck& deck) {
};
}
const auto flat_mesh =
std::ranges::find_if(deck.global_records, is_mesh_record);
if (flat_mesh != deck.global_records.end()) {
const auto flat_record =
std::ranges::find_if(deck.global_records, is_flat_model_record);
if (flat_record != deck.global_records.end()) {
return failure(
"abaqus.semantic.mixed_mesh_organization",
"Flat mesh records cannot be mixed with Part/Assembly input.",
flat_mesh->source);
"Flat model records cannot be mixed with Part/Assembly input.",
flat_record->source);
}
std::set<std::string, std::less<>> part_names;
for (const ParsedPart& part : deck.parts) {
if (!part_names.insert(part.name).second) {
return failure(
"abaqus.semantic.duplicate_part",
"Part name '" + part.name + "' is defined more than once.",
part.source);
}
}
if (!deck.assembly.has_value()) {
+121 -9
View File
@@ -8,6 +8,7 @@
#include <fstream>
#include <initializer_list>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
@@ -21,6 +22,7 @@ enum class Scope { global, part, assembly, instance, step };
struct KeywordLine final {
std::string keyword;
std::map<std::string, std::string, std::less<>> parameters;
std::set<std::string, std::less<>> flag_parameters;
SourceLocation source;
};
@@ -188,6 +190,38 @@ std::optional<ParseDeckResult> validate_parameter_names(
return std::nullopt;
}
std::optional<ParseDeckResult> unsupported_parameter_value(
const KeywordLine& keyword,
std::string_view name);
std::optional<ParseDeckResult> validate_parameter_schema(
const KeywordLine& keyword,
const std::initializer_list<std::string_view> valued,
const std::initializer_list<std::string_view> flags = {}) {
for (const auto& [name, value] : keyword.parameters) {
if (is_allowed_parameter(name, valued)) {
if (keyword.flag_parameters.contains(name) || value.empty()) {
return syntax_failure(
"abaqus.syntax.invalid_parameter",
"Parameter '" + name + "' on *" + keyword.keyword +
" requires a nonempty value.",
keyword.source);
}
} else if (is_allowed_parameter(name, flags)) {
if (!keyword.flag_parameters.contains(name)) {
return unsupported_parameter_value(keyword, name);
}
} else {
return syntax_failure(
"abaqus.syntax.unsupported_parameter",
"Unsupported parameter '" + name + "' on *" +
keyword.keyword + ".",
keyword.source);
}
}
return std::nullopt;
}
std::optional<ParseDeckResult> unsupported_parameter_value(
const KeywordLine& keyword,
const std::string_view name) {
@@ -201,10 +235,10 @@ std::optional<ParseDeckResult> unsupported_parameter_value(
std::optional<ParseDeckResult> validate_noop_parameters(
const KeywordLine& keyword) {
if (keyword.keyword == "HEADING") {
return validate_parameter_names(keyword, {});
return validate_parameter_schema(keyword, {});
}
if (keyword.keyword == "PREPRINT") {
if (auto error = validate_parameter_names(
if (auto error = validate_parameter_schema(
keyword, {"ECHO", "MODEL", "HISTORY", "CONTACT"})) {
return error;
}
@@ -217,8 +251,8 @@ std::optional<ParseDeckResult> validate_noop_parameters(
return std::nullopt;
}
if (keyword.keyword == "RESTART") {
if (auto error =
validate_parameter_names(keyword, {"WRITE", "FREQUENCY"})) {
if (auto error = validate_parameter_schema(
keyword, {"FREQUENCY"}, {"WRITE"})) {
return error;
}
if (const std::string* write = parameter(keyword, "WRITE");
@@ -239,8 +273,8 @@ std::optional<ParseDeckResult> validate_noop_parameters(
return std::nullopt;
}
if (keyword.keyword == "OUTPUT") {
if (auto error = validate_parameter_names(
keyword, {"FIELD", "HISTORY", "VARIABLE"})) {
if (auto error = validate_parameter_schema(
keyword, {"VARIABLE"}, {"FIELD", "HISTORY"})) {
return error;
}
const bool field = keyword.parameters.contains("FIELD");
@@ -280,6 +314,7 @@ std::optional<KeywordLine> parse_keyword_line(
KeywordLine parsed{
uppercase_ascii(fields[0]),
{},
{},
source,
};
for (std::size_t index = 1; index < fields.size(); ++index) {
@@ -312,6 +347,9 @@ std::optional<KeywordLine> parse_keyword_line(
source);
return std::nullopt;
}
if (equals == std::string_view::npos) {
parsed.flag_parameters.insert(key);
}
}
return parsed;
}
@@ -418,8 +456,8 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"*STEP is only valid in global input scope.",
source);
}
if (auto error =
validate_parameter_names(keyword, {"NAME", "NLGEOM"})) {
if (auto error = validate_parameter_schema(
keyword, {"NAME", "NLGEOM"})) {
return std::move(*error);
}
if (const std::string* name = parameter(keyword, "NAME");
@@ -481,6 +519,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"*PART is only valid in global input scope.",
source);
}
if (auto error = validate_parameter_schema(keyword, {"NAME"})) {
return std::move(*error);
}
const std::string* name = parameter(keyword, "NAME");
if (name == nullptr || name->empty()) {
return missing_parameter(keyword, "NAME");
@@ -497,6 +538,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"*END PART does not match an open *PART.",
source);
}
if (auto error = validate_parameter_schema(keyword, {})) {
return std::move(*error);
}
deck.parts.push_back(std::move(*current_part));
current_part.reset();
scope = Scope::global;
@@ -510,6 +554,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"*ASSEMBLY is only valid in global input scope.",
source);
}
if (auto error = validate_parameter_schema(keyword, {"NAME"})) {
return std::move(*error);
}
if (deck.assembly.has_value() ||
current_assembly.has_value()) {
return syntax_failure(
@@ -534,6 +581,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"*END ASSEMBLY does not match an open *ASSEMBLY.",
source);
}
if (auto error = validate_parameter_schema(keyword, {})) {
return std::move(*error);
}
deck.assembly = std::move(*current_assembly);
current_assembly.reset();
scope = Scope::global;
@@ -548,6 +598,10 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"*INSTANCE is only valid in an open *ASSEMBLY.",
source);
}
if (auto error =
validate_parameter_schema(keyword, {"NAME", "PART"})) {
return std::move(*error);
}
const std::string* name = parameter(keyword, "NAME");
if (name == nullptr || name->empty()) {
return missing_parameter(keyword, "NAME");
@@ -571,6 +625,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"*END INSTANCE does not match an open *INSTANCE.",
source);
}
if (auto error = validate_parameter_schema(keyword, {})) {
return std::move(*error);
}
current_assembly->instances.push_back(
std::move(*current_instance));
current_instance.reset();
@@ -668,6 +725,59 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
keyword.keyword != "BOUNDARY" && keyword.keyword != "CLOAD") {
return invalid_record_scope(keyword);
}
if (scope != Scope::instance) {
const bool mesh_scope = scope == Scope::global || scope == Scope::part;
const bool set_scope = mesh_scope || scope == Scope::assembly;
if ((keyword.keyword == "NODE" || keyword.keyword == "ELEMENT" ||
keyword.keyword == "BEAM GENERAL SECTION" ||
keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") &&
!mesh_scope) {
return invalid_record_scope(keyword);
}
if ((keyword.keyword == "NSET" || keyword.keyword == "ELSET") &&
!set_scope) {
return invalid_record_scope(keyword);
}
if ((keyword.keyword == "MATERIAL" ||
keyword.keyword == "ELASTIC") &&
scope != Scope::global) {
return invalid_record_scope(keyword);
}
}
if (scope != Scope::instance) {
std::optional<ParseDeckResult> parameter_error;
if (keyword.keyword == "NODE" || keyword.keyword == "ELASTIC" ||
keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") {
parameter_error = validate_parameter_schema(keyword, {});
} else if (keyword.keyword == "ELEMENT") {
parameter_error = validate_parameter_schema(
keyword, {"TYPE", "ELSET"});
} else if (keyword.keyword == "NSET") {
parameter_error = scope == Scope::assembly
? validate_parameter_schema(
keyword,
{"NSET", "INSTANCE"},
{"GENERATE"})
: validate_parameter_schema(
keyword, {"NSET"}, {"GENERATE"});
} else if (keyword.keyword == "ELSET") {
parameter_error = scope == Scope::assembly
? validate_parameter_schema(
keyword,
{"ELSET", "INSTANCE"},
{"GENERATE"})
: validate_parameter_schema(
keyword, {"ELSET"}, {"GENERATE"});
} else if (keyword.keyword == "MATERIAL") {
parameter_error = validate_parameter_schema(keyword, {"NAME"});
} else if (keyword.keyword == "BEAM GENERAL SECTION") {
parameter_error = validate_parameter_schema(
keyword, {"SECTION", "ELSET", "MATERIAL"});
}
if (parameter_error.has_value()) {
return std::move(*parameter_error);
}
}
DeckRecord next_record{
std::move(keyword.keyword),
@@ -678,7 +788,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
switch (scope) {
case Scope::global:
deck.global_records.push_back(std::move(next_record));
current_record = &deck.global_records.back();
if (deck.global_records.back().keyword != "MATERIAL") {
current_record = &deck.global_records.back();
}
break;
case Scope::part:
current_part->records.push_back(std::move(next_record));
+49 -14
View File
@@ -195,7 +195,7 @@ private:
std::optional<double> parse_real(
const std::string_view text,
const DeckRecord& record,
const SourceLocation& source,
const std::string_view purpose) {
double value = 0.0;
const auto parsed = std::from_chars(
@@ -209,7 +209,7 @@ private:
"abaqus.semantic.invalid_number",
"Invalid " + std::string{purpose} + " value '" +
std::string{text} + "'.",
record.source);
source);
return std::nullopt;
}
return value;
@@ -323,11 +323,22 @@ private:
if (record.keyword != "NODE") {
continue;
}
if (record.data.empty()) {
add_error(
"abaqus.semantic.invalid_node_data",
"*NODE requires at least one data row.",
record.source);
continue;
}
for (std::size_t row_index = 0; row_index < record.data.size();
++row_index) {
const auto& row = record.data[row_index];
const SourceLocation source = data_source(record, row_index);
if (row.size() < 4U) {
if (row.size() != 4U || std::ranges::any_of(
row,
[](const std::string& field) {
return field.empty();
})) {
add_error(
"abaqus.semantic.invalid_node_data",
"*NODE requires a label and three coordinates.",
@@ -337,9 +348,9 @@ private:
const auto label =
parse_label(row[0], "node", source);
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");
const auto x = parse_real(row[1], source, "node coordinate");
const auto y = parse_real(row[2], source, "node coordinate");
const auto z = parse_real(row[3], source, "node coordinate");
if (!label.has_value() || !x.has_value() || !y.has_value() ||
!z.has_value()) {
continue;
@@ -385,6 +396,10 @@ private:
std::vector<std::int64_t>& labels = target[set.set_name];
if (assembly_scope) {
labels = set.sorted_unique_labels;
if (set.kind == ResolvedSetKind::node) {
raw_assembly_node_sets_[set.set_name] =
set.sorted_unique_labels;
}
} else {
labels.insert(
labels.end(),
@@ -435,7 +450,7 @@ private:
if (section_type == nullptr || element_set == nullptr ||
material_name == nullptr) {
valid = false;
} else if (*section_type != "GENERAL") {
} else if (uppercase_ascii(*section_type) != "GENERAL") {
add_error(
"abaqus.semantic.unsupported_section",
"Phase 1 supports only SECTION=GENERAL.",
@@ -646,19 +661,30 @@ private:
if (type == nullptr) {
continue;
}
if (*type != "B31") {
if (uppercase_ascii(*type) != "B31") {
add_error(
"abaqus.semantic.unsupported_element",
"Phase 1 supports only B31 elements.",
record.source);
continue;
}
if (record.data.empty()) {
add_error(
"abaqus.semantic.invalid_element_data",
"*ELEMENT requires at least one data row.",
record.source);
continue;
}
for (std::size_t row_index = 0; row_index < record.data.size();
++row_index) {
const auto& row = record.data[row_index];
const SourceLocation source = data_source(record, row_index);
if (row.size() < 3U) {
if (row.size() != 3U || std::ranges::any_of(
row,
[](const std::string& field) {
return field.empty();
})) {
add_error(
"abaqus.semantic.invalid_element_data",
"B31 element data requires a label and two nodes.",
@@ -701,7 +727,7 @@ private:
add_error(
"abaqus.semantic.missing_node",
"B31 element references a missing node label.",
record.source);
source);
continue;
}
@@ -751,6 +777,10 @@ private:
node_sets_[name] = members;
builder_.add_node_set({name, std::move(members)});
}
for (const auto& [name, labels] : raw_assembly_node_sets_) {
assembly_node_sets_[name] =
resolve_set<NodeId>(labels, node_ids_, name);
}
for (const auto& [name, labels] : raw_element_sets_) {
std::vector<ElementId> members =
resolve_set<ElementId>(labels, element_ids_, name);
@@ -764,15 +794,17 @@ private:
std::int64_t label = 0;
const auto parsed =
std::from_chars(target.data(), target.data() + target.size(), label);
if (parsed.ec == std::errc{} &&
if (flat_ && 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()) {
} else if (parsed.ec != std::errc{} ||
parsed.ptr != target.data() + target.size()) {
const auto& target_sets = flat_ ? node_sets_ : assembly_node_sets_;
const auto found = target_sets.find(target);
if (found != target_sets.end()) {
return found->second;
}
}
@@ -1011,8 +1043,11 @@ private:
std::map<std::int64_t, SectionAssignment> element_assignments_;
std::set<std::string, std::less<>> assigned_element_set_names_;
RawSetMap raw_node_sets_;
RawSetMap raw_assembly_node_sets_;
RawSetMap raw_element_sets_;
std::map<std::string, std::vector<NodeId>, std::less<>> node_sets_;
std::map<std::string, std::vector<NodeId>, std::less<>>
assembly_node_sets_;
std::int64_t next_node_id_ = 0;
std::int64_t next_element_id_ = 0;
std::int64_t next_material_id_ = 0;