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
+1 -1
View File
@@ -6,7 +6,7 @@
"step": 0,
"name": "abaqus-input-contract",
"status": "completed",
"summary": "Defined the normative Phase 1 Abaqus subset and a 53-case public parser/mapper fixture matrix; 28 cases pass and 25 expected failures remain for Steps 1-4.",
"summary": "Defined the normative Phase 1 Abaqus subset and expanded the public parser/mapper fixture matrix to 69 cases covering strict scopes, parameter forms, data ownership, exact mesh rows, duplicate Parts, Assembly-only hierarchical targets, and source-accurate diagnostics.",
"started_at": "2026-08-01T02:16:39+0900",
"completed_at": "2026-08-01T02:24:23+0900"
},
+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()) {
+120 -8
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));
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;
+16
View File
@@ -26,6 +26,7 @@ heading_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_defa
preprint_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
restart_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
output_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
enum_case_insensitive_valid valid valid/case_insensitive_enums.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
node_duplicate_invalid invalid invalid/node_duplicate.inp semantic abaqus.semantic.duplicate_node_label 3 - - - - - - - - - - -
element_type_invalid invalid invalid/element_wrong_type.inp semantic abaqus.semantic.unsupported_element 4 - - - - - - - - - - -
part_unclosed_invalid invalid invalid/part_unclosed.inp syntax abaqus.syntax.unclosed_part 1 - - - - - - - - - - -
@@ -52,3 +53,18 @@ preprint_parameter_invalid invalid invalid/preprint_invalid_parameter.inp syntax
restart_scope_invalid invalid invalid/restart_outside_step.inp syntax abaqus.syntax.invalid_restart_scope 1 - - - - - - - - - - -
output_parameter_invalid invalid invalid/output_invalid_parameter.inp syntax abaqus.syntax.unsupported_parameter 3 - - - - - - - - - - -
unknown_keyword_invalid invalid invalid/unknown_include.inp syntax abaqus.unsupported_keyword 1 - - - - - - - - - - -
node_parameter_invalid invalid invalid/node_unsupported_parameter.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
node_scope_invalid invalid invalid/node_wrong_scope.inp syntax abaqus.syntax.invalid_node_scope 2 - - - - - - - - - - -
material_scope_invalid invalid invalid/material_wrong_scope.inp syntax abaqus.syntax.invalid_material_scope 2 - - - - - - - - - - -
generate_form_invalid invalid invalid/generate_valued.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
flat_set_instance_invalid invalid invalid/flat_set_instance.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
global_set_hierarchy_invalid invalid invalid/global_set_hierarchical.inp semantic abaqus.semantic.mixed_mesh_organization 1 - - - - - - - - - - -
duplicate_part_invalid invalid invalid/duplicate_part.inp semantic abaqus.semantic.duplicate_part 3 - - - - - - - - - - -
node_surplus_field_invalid invalid invalid/node_surplus_field.inp semantic abaqus.semantic.invalid_node_data 2 - - - - - - - - - - -
node_empty_invalid invalid invalid/node_empty.inp semantic abaqus.semantic.invalid_node_data 1 - - - - - - - - - - -
element_surplus_field_invalid invalid invalid/element_surplus_field.inp semantic abaqus.semantic.invalid_element_data 2 - - - - - - - - - - -
element_empty_invalid invalid invalid/element_empty.inp semantic abaqus.semantic.invalid_element_data 1 - - - - - - - - - - -
hierarchical_part_target_invalid invalid invalid/hierarchical_part_set_target.inp semantic abaqus.semantic.missing_node_target 25 - - - - - - - - - - -
material_data_invalid invalid invalid/material_data.inp syntax abaqus.syntax.data_without_keyword 2 - - - - - - - - - - -
node_coordinate_invalid invalid invalid/node_invalid_coordinate.inp semantic abaqus.semantic.invalid_number 2 - - - - - - - - - - -
element_missing_node_invalid invalid invalid/element_missing_node.inp semantic abaqus.semantic.missing_node 4 - - - - - - - - - - -
1 # case_id outcome fixture expected_stage expected_code expected_line node_count element_count node_set_count element_set_count prescribed_dof_count nodal_load_count shear_source shear_area_y shear_area_z checked_node_set checked_element_set
26 preprint_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
27 restart_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
28 output_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
29 enum_case_insensitive_valid valid valid/case_insensitive_enums.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
30 node_duplicate_invalid invalid invalid/node_duplicate.inp semantic abaqus.semantic.duplicate_node_label 3 - - - - - - - - - - -
31 element_type_invalid invalid invalid/element_wrong_type.inp semantic abaqus.semantic.unsupported_element 4 - - - - - - - - - - -
32 part_unclosed_invalid invalid invalid/part_unclosed.inp syntax abaqus.syntax.unclosed_part 1 - - - - - - - - - - -
53 restart_scope_invalid invalid invalid/restart_outside_step.inp syntax abaqus.syntax.invalid_restart_scope 1 - - - - - - - - - - -
54 output_parameter_invalid invalid invalid/output_invalid_parameter.inp syntax abaqus.syntax.unsupported_parameter 3 - - - - - - - - - - -
55 unknown_keyword_invalid invalid invalid/unknown_include.inp syntax abaqus.unsupported_keyword 1 - - - - - - - - - - -
56 node_parameter_invalid invalid invalid/node_unsupported_parameter.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
57 node_scope_invalid invalid invalid/node_wrong_scope.inp syntax abaqus.syntax.invalid_node_scope 2 - - - - - - - - - - -
58 material_scope_invalid invalid invalid/material_wrong_scope.inp syntax abaqus.syntax.invalid_material_scope 2 - - - - - - - - - - -
59 generate_form_invalid invalid invalid/generate_valued.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
60 flat_set_instance_invalid invalid invalid/flat_set_instance.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
61 global_set_hierarchy_invalid invalid invalid/global_set_hierarchical.inp semantic abaqus.semantic.mixed_mesh_organization 1 - - - - - - - - - - -
62 duplicate_part_invalid invalid invalid/duplicate_part.inp semantic abaqus.semantic.duplicate_part 3 - - - - - - - - - - -
63 node_surplus_field_invalid invalid invalid/node_surplus_field.inp semantic abaqus.semantic.invalid_node_data 2 - - - - - - - - - - -
64 node_empty_invalid invalid invalid/node_empty.inp semantic abaqus.semantic.invalid_node_data 1 - - - - - - - - - - -
65 element_surplus_field_invalid invalid invalid/element_surplus_field.inp semantic abaqus.semantic.invalid_element_data 2 - - - - - - - - - - -
66 element_empty_invalid invalid invalid/element_empty.inp semantic abaqus.semantic.invalid_element_data 1 - - - - - - - - - - -
67 hierarchical_part_target_invalid invalid invalid/hierarchical_part_set_target.inp semantic abaqus.semantic.missing_node_target 25 - - - - - - - - - - -
68 material_data_invalid invalid invalid/material_data.inp syntax abaqus.syntax.data_without_keyword 2 - - - - - - - - - - -
69 node_coordinate_invalid invalid invalid/node_invalid_coordinate.inp semantic abaqus.semantic.invalid_number 2 - - - - - - - - - - -
70 element_missing_node_invalid invalid invalid/element_missing_node.inp semantic abaqus.semantic.missing_node 4 - - - - - - - - - - -
+8
View File
@@ -0,0 +1,8 @@
*PART, NAME=BeamPart
*END PART
*PART, NAME=BeamPart
*END PART
*ASSEMBLY, NAME=RootAssembly
*INSTANCE, NAME=Beam-1, PART=BeamPart
*END INSTANCE
*END ASSEMBLY
+4
View File
@@ -0,0 +1,4 @@
*ELEMENT, TYPE=B31
*STEP
*STATIC
*END STEP
+13
View File
@@ -0,0 +1,13 @@
*NODE
1, 0.0, 0.0, 0.0
*ELEMENT, TYPE=B31, ELSET=Beam
1, 1, 2
*MATERIAL, NAME=Steel
*ELASTIC
210000.0, 0.3
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
1.0, 1.0, 0.0, 1.0, 1.0
0.0, 1.0, 0.0
*STEP
*STATIC
*END STEP
@@ -0,0 +1,5 @@
*ELEMENT, TYPE=B31
1, 1, 2, 3
*STEP
*STATIC
*END STEP
+2
View File
@@ -0,0 +1,2 @@
*NSET, NSET=Fixed, INSTANCE=Beam-1
1
+2
View File
@@ -0,0 +1,2 @@
*NSET, NSET=Generated, GENERATE=YES
1, 2, 1
@@ -0,0 +1,7 @@
*NSET, NSET=Ignored
*PART, NAME=BeamPart
*END PART
*ASSEMBLY, NAME=RootAssembly
*INSTANCE, NAME=Beam-1, PART=BeamPart
*END INSTANCE
*END ASSEMBLY
@@ -0,0 +1,26 @@
*PART, NAME=BeamPart
*NODE
1, 0.0, 0.0, 0.0
2, 1.0, 0.0, 0.0
*ELEMENT, TYPE=B31, ELSET=Beam
1, 1, 2
*NSET, NSET=Fixed
1
*ELSET, ELSET=Beam
1
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
1.0, 1.0, 0.0, 1.0, 1.0
0.0, 1.0, 0.0
*END PART
*ASSEMBLY, NAME=RootAssembly
*INSTANCE, NAME=Beam-1, PART=BeamPart
*END INSTANCE
*END ASSEMBLY
*MATERIAL, NAME=Steel
*ELASTIC
200.0, 0.25
*STEP
*STATIC
*BOUNDARY
Fixed, 1, 6
*END STEP
+2
View File
@@ -0,0 +1,2 @@
*MATERIAL, NAME=Steel
unexpected data
@@ -0,0 +1,3 @@
*PART, NAME=BeamPart
*MATERIAL, NAME=Steel
*END PART
+4
View File
@@ -0,0 +1,4 @@
*NODE
*STEP
*STATIC
*END STEP
@@ -0,0 +1,5 @@
*NODE
1, invalid, 0.0, 0.0
*STEP
*STATIC
*END STEP
+5
View File
@@ -0,0 +1,5 @@
*NODE
1, 0.0, 0.0, 0.0, 9.0
*STEP
*STATIC
*END STEP
@@ -0,0 +1,2 @@
*NODE, EXTRA=1
1, 0.0, 0.0, 0.0
+4
View File
@@ -0,0 +1,4 @@
*ASSEMBLY, NAME=RootAssembly
*NODE
1, 0.0, 0.0, 0.0
*END ASSEMBLY
+1 -1
View File
@@ -4,7 +4,7 @@
1, 0.0, 0.0, 0.0
** Comments and blank lines do not terminate the current data record.
2, 1.0, 0.0, 0.0,
2, 1.0, 0.0, 0.0
*eLeMeNt, TYPE=B31, elset=Beam
1, 1, 2
*NSET, NSET=Fixed
+22
View File
@@ -0,0 +1,22 @@
*NODE
1, 0.0, 0.0, 0.0
2, 1.0, 0.0, 0.0
*ELEMENT, TYPE=b31, ELSET=Beam
1, 1, 2
*NSET, NSET=Fixed
1
*ELSET, ELSET=Beam
1
*MATERIAL, NAME=Steel
*ELASTIC
200.0, 0.25
*BEAM GENERAL SECTION, SECTION=general, ELSET=Beam, MATERIAL=Steel
1.0, 1.0, 0.0, 1.0, 1.0
0.0, 1.0, 0.0
*BOUNDARY
Fixed, 1, 6
*STEP, NAME=Load
*STATIC
*CLOAD
2, 2, -1.0
*END STEP
@@ -173,6 +173,128 @@ TEST(DeckToDomain, NormalizesFlatAndSingleInstanceDecksEquivalently) {
fesa::ShearPropertySource::phase1_default);
}
TEST(DeckToDomain, RequiresExactNodeAndB31DataWidthsAndNonemptyRecords) {
struct Case final {
std::string_view name;
std::string_view mesh;
std::string_view code;
};
const Case cases[]{
{
"node-surplus-field",
"*NODE\n1, 0.0, 0.0, 0.0, 9.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2\n",
"abaqus.semantic.invalid_node_data",
},
{
"node-empty",
"*NODE\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2\n",
"abaqus.semantic.invalid_node_data",
},
{
"element-surplus-field",
"*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2, 3\n",
"abaqus.semantic.invalid_element_data",
},
{
"element-empty",
"*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n",
"abaqus.semantic.invalid_element_data",
},
};
for (const auto& test_case : cases) {
const TemporaryDeck input{
"fesa-exact-mesh-data-" + std::string{test_case.name} + ".inp",
std::string{test_case.mesh} +
"*ELSET, ELSET=Beam\n1\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n210000.0, 0.3\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, "
"MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n0.0, 1.0, 0.0\n"
"*STEP\n*STATIC\n*END STEP\n",
};
const auto result = parse_and_map(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.domain.has_value());
EXPECT_TRUE(has_diagnostic(result, test_case.code));
}
}
TEST(DeckToDomain, ReportsMeshDataErrorsAtTheOffendingRows) {
const TemporaryDeck invalid_coordinate{
"fesa-node-coordinate-source.inp",
"*NODE\n"
"1, invalid, 0.0, 0.0\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const TemporaryDeck missing_node{
"fesa-element-node-source.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
"1, 1, 2\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const auto coordinate_result = parse_and_map(invalid_coordinate.path());
const auto missing_node_result = parse_and_map(missing_node.path());
const fesa::Diagnostic* coordinate =
find_diagnostic(coordinate_result, "abaqus.semantic.invalid_number");
ASSERT_NE(coordinate, nullptr);
ASSERT_TRUE(coordinate->source.has_value());
EXPECT_EQ(coordinate->source->line, 2U);
const fesa::Diagnostic* node =
find_diagnostic(missing_node_result, "abaqus.semantic.missing_node");
ASSERT_NE(node, nullptr);
ASSERT_TRUE(node->source.has_value());
EXPECT_EQ(node->source->line, 4U);
}
TEST(DeckToDomain, AcceptsCaseInsensitiveElementAndSectionValues) {
const TemporaryDeck input{
"fesa-case-insensitive-enums.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=b31, ELSET=Beam\n"
"1, 1, 2\n"
"*ELSET, ELSET=Beam\n"
"1\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*BEAM GENERAL SECTION, SECTION=general, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
EXPECT_EQ(result.domain->beam_elements().size(), 1U);
}
TEST(ActiveInstance, ExcludesPartsNotReferencedByTheInstance) {
const TemporaryDeck input{
"fesa-active-instance.inp",
@@ -406,6 +528,42 @@ TEST(Boundary, CanonicalizesIdenticalGlobalAndStepPrescriptions) {
}
}
TEST(Boundary, HierarchicalTargetsRequireAssemblySets) {
const TemporaryDeck input{
"fesa-hierarchical-part-set-target.inp",
"*PART, NAME=BeamPart\n"
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
"1, 1, 2\n"
"*NSET, NSET=Fixed\n"
"1\n"
"*ELSET, ELSET=Beam\n"
"1\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*STEP\n"
"*STATIC\n"
"*BOUNDARY\n"
"Fixed, 1, 6\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
EXPECT_TRUE(has_diagnostic(result, "abaqus.semantic.missing_node_target"));
}
TEST(Boundary, ReportsConflictAtTheConflictingDataRow) {
const auto result =
parse_and_map(fixture_path("invalid/boundary_conflict.inp"));
@@ -130,6 +130,37 @@ TEST(ActiveInput, RejectsMixedFlatAndHierarchicalMeshAtTheFlatRecord) {
EXPECT_EQ(diagnostic.source->file, fixture_path("invalid/mixed_mesh.inp"));
}
TEST(ActiveInput, RejectsGlobalSetsInHierarchicalInput) {
const TemporaryDeck input{
"fesa-global-set-with-hierarchy.inp",
"*NSET, NSET=Ignored\n"
"*PART, NAME=BeamPart\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"};
expect_failure(
parse(input.path()), "abaqus.semantic.mixed_mesh_organization", 1U);
}
TEST(ActiveInput, RejectsDuplicatePartNamesAtTheSecondDefinition) {
const TemporaryDeck input{
"fesa-duplicate-parts.inp",
"*PART, NAME=BeamPart\n"
"*END PART\n"
"*PART, NAME=BeamPart\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"};
expect_failure(
parse(input.path()), "abaqus.semantic.duplicate_part", 3U);
}
TEST(ActiveInput, RejectsHierarchicalInputWithoutAnAssembly) {
const TemporaryDeck input{
"fesa-missing-assembly.inp",
+209 -1
View File
@@ -74,7 +74,7 @@ TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
EXPECT_EQ(nodes.data[0], (std::vector<std::string>{
"1", "0.0", "0.0", "0.0"}));
EXPECT_EQ(nodes.data[1], (std::vector<std::string>{
"2", "1.0", "0.0", "0.0", ""}));
"2", "1.0", "0.0", "0.0"}));
EXPECT_EQ(nodes.source.file, path);
EXPECT_EQ(nodes.source.line, 3U);
EXPECT_EQ(nodes.source.column, 1U);
@@ -121,6 +121,214 @@ TEST(AbaqusParser, RejectsUnsupportedKeywordsInsteadOfIgnoringThem) {
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
}
TEST(AbaqusParser, RejectsUnsupportedParametersOnSupportedKeywords) {
struct Case final {
std::string_view name;
std::string_view contents;
std::size_t line;
};
const Case cases[]{
{"node", "*NODE, EXTRA=1\n1, 0.0, 0.0, 0.0\n", 1U},
{"element", "*ELEMENT, TYPE=B31, EXTRA=1\n1, 1, 2\n", 1U},
{"part", "*PART, NAME=P, EXTRA=1\n*END PART\n", 1U},
{"end-part", "*PART, NAME=P\n*END PART, EXTRA=1\n", 2U},
{
"assembly",
"*ASSEMBLY, NAME=A, EXTRA=1\n*END ASSEMBLY\n",
1U,
},
{
"end-assembly",
"*ASSEMBLY, NAME=A\n*END ASSEMBLY, EXTRA=1\n",
2U,
},
{
"instance",
"*ASSEMBLY, NAME=A\n"
"*INSTANCE, NAME=I, PART=P, EXTRA=1\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n",
2U,
},
{
"end-instance",
"*ASSEMBLY, NAME=A\n"
"*INSTANCE, NAME=I, PART=P\n"
"*END INSTANCE, EXTRA=1\n"
"*END ASSEMBLY\n",
3U,
},
{"nset", "*NSET, NSET=S, EXTRA=1\n1\n", 1U},
{"elset", "*ELSET, ELSET=S, EXTRA=1\n1\n", 1U},
{"material", "*MATERIAL, NAME=M, EXTRA=1\n", 1U},
{
"elastic",
"*MATERIAL, NAME=M\n*ELASTIC, EXTRA=1\n1.0, 0.3\n",
2U,
},
{
"beam-general-section",
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, "
"MATERIAL=M, EXTRA=1\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n",
1U,
},
{
"transverse-shear-stiffness",
"*TRANSVERSE SHEAR STIFFNESS, EXTRA=1\n1.0, 1.0, 0.0\n",
1U,
},
};
for (const auto& test_case : cases) {
const TemporaryDeck input{
"fesa-parser-unsupported-parameter-" +
std::string{test_case.name} + ".inp",
test_case.contents,
};
const auto result = fesa::parse_deck(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.deck.has_value());
ASSERT_EQ(result.diagnostics.size(), 1U);
EXPECT_EQ(
result.diagnostics[0].code,
"abaqus.syntax.unsupported_parameter");
ASSERT_TRUE(result.diagnostics[0].source.has_value());
EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
}
}
TEST(AbaqusParser, RejectsInvalidParameterFormsAndSetScopeOptions) {
struct Case final {
std::string_view name;
std::string_view contents;
std::string_view code;
};
const Case cases[]{
{
"generate-valued",
"*NSET, NSET=S, GENERATE=YES\n1, 2, 1\n",
"abaqus.syntax.unsupported_parameter",
},
{
"generate-empty-valued",
"*NSET, NSET=S, GENERATE=\n1, 2, 1\n",
"abaqus.syntax.unsupported_parameter",
},
{
"empty-element-set",
"*ELEMENT, TYPE=B31, ELSET=\n1, 1, 2\n",
"abaqus.syntax.invalid_parameter",
},
{
"instance-on-flat-set",
"*NSET, NSET=S, INSTANCE=I\n1\n",
"abaqus.syntax.unsupported_parameter",
},
{
"instance-on-part-set",
"*PART, NAME=P\n"
"*NSET, NSET=S, INSTANCE=I\n"
"1\n"
"*END PART\n",
"abaqus.syntax.unsupported_parameter",
},
};
for (const auto& test_case : cases) {
const TemporaryDeck input{
"fesa-parser-parameter-form-" + std::string{test_case.name} +
".inp",
test_case.contents,
};
const auto result = fesa::parse_deck(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.deck.has_value());
ASSERT_EQ(result.diagnostics.size(), 1U);
EXPECT_EQ(result.diagnostics[0].code, test_case.code);
ASSERT_TRUE(result.diagnostics[0].source.has_value());
EXPECT_EQ(result.diagnostics[0].source->line, 1U +
(test_case.name == "instance-on-part-set" ? 1U : 0U));
}
}
TEST(AbaqusParser, RejectsModelKeywordsOutsideTheirDocumentedScopes) {
struct Case final {
std::string_view name;
std::string_view contents;
std::string_view code;
std::size_t line;
};
const Case cases[]{
{
"node-in-assembly",
"*ASSEMBLY, NAME=A\n"
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*END ASSEMBLY\n",
"abaqus.syntax.invalid_node_scope",
2U,
},
{
"material-in-part",
"*PART, NAME=P\n"
"*MATERIAL, NAME=M\n"
"*END PART\n",
"abaqus.syntax.invalid_material_scope",
2U,
},
{
"section-in-assembly",
"*ASSEMBLY, NAME=A\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, MATERIAL=M\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*END ASSEMBLY\n",
"abaqus.syntax.invalid_section_scope",
2U,
},
};
for (const auto& test_case : cases) {
const TemporaryDeck input{
"fesa-parser-invalid-scope-" + std::string{test_case.name} +
".inp",
test_case.contents,
};
const auto result = fesa::parse_deck(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.deck.has_value());
ASSERT_EQ(result.diagnostics.size(), 1U);
EXPECT_EQ(result.diagnostics[0].code, test_case.code);
ASSERT_TRUE(result.diagnostics[0].source.has_value());
EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
}
}
TEST(AbaqusParser, RejectsDataRowsOnMaterial) {
const TemporaryDeck input{
"fesa-parser-material-data.inp",
"*MATERIAL, NAME=Steel\n"
"unexpected data\n"};
const auto result = fesa::parse_deck(input.path());
EXPECT_FALSE(result.deck.has_value());
ASSERT_EQ(result.diagnostics.size(), 1U);
EXPECT_EQ(
result.diagnostics[0].code,
"abaqus.syntax.data_without_keyword");
ASSERT_TRUE(result.diagnostics[0].source.has_value());
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
}
TEST(ScopedDeck, PreservesPartAssemblyAndInstanceScopes) {
const auto path =
fixture_path("minimal_part_instance_cantilever.inp");