feat(abaqus-subset-completion): step 4 — step-bc-load-and-noop-directives

This commit is contained in:
KOKO\Mimi
2026-08-01 03:23:25 +09:00
parent af076c39a3
commit 94c83a39c5
4 changed files with 785 additions and 68 deletions
+322 -3
View File
@@ -2,8 +2,11 @@
#include <algorithm>
#include <array>
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <initializer_list>
#include <optional>
#include <string>
#include <string_view>
@@ -13,7 +16,7 @@
namespace fesa {
namespace {
enum class Scope { global, part, assembly, instance };
enum class Scope { global, part, assembly, instance, step };
struct KeywordLine final {
std::string keyword;
@@ -21,6 +24,10 @@ struct KeywordLine final {
SourceLocation source;
};
const std::string* parameter(
const KeywordLine& keyword,
std::string_view name);
std::string_view trim(const std::string_view value) {
constexpr std::string_view whitespace{" \t\f\v\r\n"};
const std::size_t first = value.find_first_not_of(whitespace);
@@ -93,17 +100,170 @@ bool is_supported_record(const std::string_view keyword) {
std::string_view{"ELASTIC"},
std::string_view{"ELEMENT"},
std::string_view{"ELSET"},
std::string_view{"END STEP"},
std::string_view{"HEADING"},
std::string_view{"MATERIAL"},
std::string_view{"NODE"},
std::string_view{"NSET"},
std::string_view{"OUTPUT"},
std::string_view{"PREPRINT"},
std::string_view{"RESTART"},
std::string_view{"STATIC"},
std::string_view{"STEP"},
std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
};
return std::ranges::find(supported, keyword) != supported.end();
}
bool is_known_keyword(const std::string_view keyword) {
constexpr std::array scope_keywords{
std::string_view{"ASSEMBLY"},
std::string_view{"END ASSEMBLY"},
std::string_view{"END INSTANCE"},
std::string_view{"END PART"},
std::string_view{"INSTANCE"},
std::string_view{"PART"},
};
return is_supported_record(keyword) ||
std::ranges::find(scope_keywords, keyword) != scope_keywords.end();
}
ParseDeckResult invalid_record_scope(const KeywordLine& keyword) {
std::string code = "abaqus.syntax.invalid_step_scope";
if (keyword.keyword == "NODE") {
code = "abaqus.syntax.invalid_node_scope";
} else if (keyword.keyword == "ELEMENT") {
code = "abaqus.syntax.invalid_element_scope";
} else if (keyword.keyword == "NSET" || keyword.keyword == "ELSET") {
code = "abaqus.syntax.invalid_set_scope";
} else if (keyword.keyword == "MATERIAL" ||
keyword.keyword == "ELASTIC") {
code = "abaqus.syntax.invalid_material_scope";
} else if (keyword.keyword == "BEAM GENERAL SECTION") {
code = "abaqus.syntax.invalid_section_scope";
} else if (keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") {
code = "abaqus.syntax.invalid_transverse_shear_scope";
} else if (keyword.keyword == "BOUNDARY") {
code = "abaqus.syntax.invalid_boundary_scope";
} else if (keyword.keyword == "CLOAD") {
code = "abaqus.syntax.invalid_cload_scope";
} else if (keyword.keyword == "HEADING") {
code = "abaqus.syntax.invalid_heading_scope";
} else if (keyword.keyword == "PREPRINT") {
code = "abaqus.syntax.invalid_preprint_scope";
} else if (keyword.keyword == "RESTART") {
code = "abaqus.syntax.invalid_restart_scope";
} else if (keyword.keyword == "OUTPUT") {
code = "abaqus.syntax.invalid_output_scope";
} else if (keyword.keyword == "PART") {
code = "abaqus.syntax.invalid_part_scope";
} else if (keyword.keyword == "ASSEMBLY") {
code = "abaqus.syntax.invalid_assembly_scope";
} else if (keyword.keyword == "INSTANCE") {
code = "abaqus.syntax.invalid_instance_scope";
}
return syntax_failure(
std::move(code),
"*" + keyword.keyword + " is invalid in the current input scope.",
keyword.source);
}
bool is_allowed_parameter(
const std::string_view name,
const std::initializer_list<std::string_view> allowed) {
return std::ranges::find(allowed, name) != allowed.end();
}
std::optional<ParseDeckResult> validate_parameter_names(
const KeywordLine& keyword,
const std::initializer_list<std::string_view> allowed) {
for (const auto& [name, value] : keyword.parameters) {
static_cast<void>(value);
if (!is_allowed_parameter(name, allowed)) {
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) {
return syntax_failure(
"abaqus.syntax.unsupported_parameter",
"Unsupported value for parameter '" + std::string{name} +
"' on *" + keyword.keyword + ".",
keyword.source);
}
std::optional<ParseDeckResult> validate_noop_parameters(
const KeywordLine& keyword) {
if (keyword.keyword == "HEADING") {
return validate_parameter_names(keyword, {});
}
if (keyword.keyword == "PREPRINT") {
if (auto error = validate_parameter_names(
keyword, {"ECHO", "MODEL", "HISTORY", "CONTACT"})) {
return error;
}
for (const auto& [name, value] : keyword.parameters) {
const std::string normalized = uppercase_ascii(value);
if (normalized != "YES" && normalized != "NO") {
return unsupported_parameter_value(keyword, name);
}
}
return std::nullopt;
}
if (keyword.keyword == "RESTART") {
if (auto error =
validate_parameter_names(keyword, {"WRITE", "FREQUENCY"})) {
return error;
}
if (const std::string* write = parameter(keyword, "WRITE");
write != nullptr && !write->empty()) {
return unsupported_parameter_value(keyword, "WRITE");
}
if (const std::string* frequency = parameter(keyword, "FREQUENCY");
frequency != nullptr) {
std::int64_t value = 0;
const auto parsed = std::from_chars(
frequency->data(), frequency->data() + frequency->size(), value);
if (frequency->empty() || parsed.ec != std::errc{} ||
parsed.ptr != frequency->data() + frequency->size() ||
value < 0) {
return unsupported_parameter_value(keyword, "FREQUENCY");
}
}
return std::nullopt;
}
if (keyword.keyword == "OUTPUT") {
if (auto error = validate_parameter_names(
keyword, {"FIELD", "HISTORY", "VARIABLE"})) {
return error;
}
const bool field = keyword.parameters.contains("FIELD");
const bool history = keyword.parameters.contains("HISTORY");
if (field == history) {
return unsupported_parameter_value(keyword, "FIELD/HISTORY");
}
const std::string* flag = parameter(
keyword, field ? std::string_view{"FIELD"}
: std::string_view{"HISTORY"});
if (flag == nullptr || !flag->empty()) {
return unsupported_parameter_value(
keyword, field ? "FIELD" : "HISTORY");
}
if (const std::string* variable = parameter(keyword, "VARIABLE");
variable != nullptr && uppercase_ascii(*variable) != "PRESELECT") {
return unsupported_parameter_value(keyword, "VARIABLE");
}
return std::nullopt;
}
return std::nullopt;
}
std::optional<KeywordLine> parse_keyword_line(
const std::string_view line,
const SourceLocation& source,
@@ -190,6 +350,8 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
std::optional<ParsedPart> current_part;
std::optional<ParsedAssembly> current_assembly;
std::optional<ParsedInstance> current_instance;
std::optional<SourceLocation> current_step_source;
bool completed_step = false;
DeckRecord* current_record = nullptr;
std::string line;
@@ -249,6 +411,69 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
KeywordLine& keyword = *parsed;
current_record = nullptr;
if (keyword.keyword == "STEP") {
if (scope != Scope::global) {
return syntax_failure(
"abaqus.syntax.invalid_step_scope",
"*STEP is only valid in global input scope.",
source);
}
if (auto error =
validate_parameter_names(keyword, {"NAME", "NLGEOM"})) {
return std::move(*error);
}
if (const std::string* name = parameter(keyword, "NAME");
name != nullptr && name->empty()) {
return syntax_failure(
"abaqus.syntax.invalid_parameter",
"*STEP parameter NAME requires a nonempty value.",
source);
}
if (const std::string* nlgeom = parameter(keyword, "NLGEOM");
nlgeom != nullptr && nlgeom->empty()) {
return syntax_failure(
"abaqus.syntax.invalid_parameter",
"*STEP parameter NLGEOM requires a value.",
source);
}
deck.global_records.push_back({
std::move(keyword.keyword),
std::move(keyword.parameters),
{},
source,
});
current_step_source = source;
scope = Scope::step;
continue;
}
if (keyword.keyword == "END STEP") {
if (auto error = validate_parameter_names(keyword, {})) {
return std::move(*error);
}
if (scope != Scope::step || !current_step_source.has_value()) {
return syntax_failure(
"abaqus.syntax.unexpected_end_step",
"*END STEP does not match an open *STEP.",
source);
}
deck.global_records.push_back({
std::move(keyword.keyword),
std::move(keyword.parameters),
{},
source,
});
current_step_source.reset();
completed_step = true;
scope = Scope::global;
continue;
}
if (scope == Scope::global && completed_step &&
is_known_keyword(keyword.keyword)) {
return invalid_record_scope(keyword);
}
if (keyword.keyword == "PART") {
if (scope != Scope::global) {
return syntax_failure(
@@ -353,12 +578,96 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
continue;
}
if (keyword.keyword == "HEADING" ||
keyword.keyword == "PREPRINT") {
if (scope != Scope::global) {
return syntax_failure(
keyword.keyword == "HEADING"
? "abaqus.syntax.invalid_heading_scope"
: "abaqus.syntax.invalid_preprint_scope",
"*" + keyword.keyword +
" is only valid in global input scope.",
source);
}
if (auto error = validate_noop_parameters(keyword)) {
return std::move(*error);
}
deck.global_records.push_back({
std::move(keyword.keyword),
std::move(keyword.parameters),
{},
source,
});
if (deck.global_records.back().keyword == "HEADING") {
current_record = &deck.global_records.back();
}
continue;
}
if (keyword.keyword == "RESTART" || keyword.keyword == "OUTPUT") {
if (scope != Scope::step) {
return syntax_failure(
keyword.keyword == "RESTART"
? "abaqus.syntax.invalid_restart_scope"
: "abaqus.syntax.invalid_output_scope",
"*" + keyword.keyword +
" is only valid inside *STEP.",
source);
}
if (auto error = validate_noop_parameters(keyword)) {
return std::move(*error);
}
deck.global_records.push_back({
std::move(keyword.keyword),
std::move(keyword.parameters),
{},
source,
});
continue;
}
if (keyword.keyword == "STATIC") {
if (scope != Scope::step) {
return syntax_failure(
"abaqus.syntax.invalid_step_scope",
"*STATIC is only valid inside *STEP.",
source);
}
if (auto error = validate_parameter_names(keyword, {})) {
return std::move(*error);
}
} else if (keyword.keyword == "CLOAD") {
if (scope != Scope::step) {
return syntax_failure(
"abaqus.syntax.invalid_cload_scope",
"*CLOAD is only valid inside *STEP.",
source);
}
if (auto error = validate_parameter_names(keyword, {})) {
return std::move(*error);
}
} else if (keyword.keyword == "BOUNDARY") {
if (scope != Scope::global && scope != Scope::step) {
return syntax_failure(
"abaqus.syntax.invalid_boundary_scope",
"*BOUNDARY is valid only in global or Step scope.",
source);
}
if (auto error = validate_parameter_names(keyword, {})) {
return std::move(*error);
}
}
if (!is_supported_record(keyword.keyword)) {
return syntax_failure(
"abaqus.unsupported_keyword",
"Unsupported Abaqus keyword *" + keyword.keyword + ".",
source);
}
if (scope == Scope::step && keyword.keyword != "STATIC" &&
keyword.keyword != "BOUNDARY" && keyword.keyword != "CLOAD") {
return invalid_record_scope(keyword);
}
DeckRecord next_record{
std::move(keyword.keyword),
@@ -385,6 +694,10 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"abaqus.syntax.instance_local_keyword",
"Keyword records inside *INSTANCE are unsupported.",
source);
case Scope::step:
deck.global_records.push_back(std::move(next_record));
current_record = &deck.global_records.back();
break;
}
}
@@ -414,6 +727,12 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
"Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
current_assembly->source);
}
if (current_step_source.has_value()) {
return syntax_failure(
"abaqus.syntax.unclosed_step",
"Abaqus *STEP is not closed by *END STEP.",
*current_step_source);
}
return {std::move(deck), {}};
}
+215 -56
View File
@@ -51,6 +51,16 @@ bool parse_number(const std::string_view text, double& value) {
parsed.ptr == text.data() + text.size() && std::isfinite(value);
}
std::string uppercase_ascii(std::string value) {
std::ranges::transform(value, value.begin(), [](const char character) {
if (character >= 'a' && character <= 'z') {
return static_cast<char>(character - 'a' + 'A');
}
return character;
});
return value;
}
class DeckMapper final {
public:
explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}
@@ -141,8 +151,8 @@ private:
std::optional<std::int64_t> parse_label(
const std::string_view text,
const DeckRecord& record,
const std::string_view purpose) {
const std::string_view purpose,
const SourceLocation& source) {
std::int64_t value = 0;
const auto parsed =
std::from_chars(text.data(), text.data() + text.size(), value);
@@ -152,28 +162,35 @@ private:
"abaqus.semantic.invalid_label",
"Invalid " + std::string{purpose} + " label '" +
std::string{text} + "'.",
record.source);
source);
return std::nullopt;
}
return value;
}
std::optional<std::int64_t> parse_label(
const std::string_view text,
const DeckRecord& record,
const std::string_view purpose) {
return parse_label(text, purpose, record.source);
}
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) {
const SourceLocation& source) {
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 < 1 ||
value > 6) {
add_error(
"abaqus.semantic.invalid_dof",
"Phase 1 supports only degrees of freedom 1 through 6.",
record.source);
source);
return std::nullopt;
}
return static_cast<std::uint8_t>(*value);
return static_cast<std::uint8_t>(value);
}
std::optional<double> parse_real(
@@ -187,7 +204,7 @@ private:
value,
std::chars_format::general);
if (parsed.ec != std::errc{} ||
parsed.ptr != text.data() + text.size()) {
parsed.ptr != text.data() + text.size() || !std::isfinite(value)) {
add_error(
"abaqus.semantic.invalid_number",
"Invalid " + std::string{purpose} + " value '" +
@@ -306,16 +323,20 @@ private:
if (record.keyword != "NODE") {
continue;
}
for (const auto& row : record.data) {
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) {
add_error(
"abaqus.semantic.invalid_node_data",
"*NODE requires a label and three coordinates.",
record.source);
source);
continue;
}
const auto label = parse_label(row[0], record, "node");
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");
@@ -324,6 +345,15 @@ private:
continue;
}
if (node_ids_.contains(*label)) {
add_error(
"abaqus.semantic.duplicate_node_label",
"Node label " + std::to_string(*label) +
" is defined more than once in its mesh scope.",
source);
continue;
}
const NodeId id{next_node_id_++};
node_ids_.emplace(*label, id);
builder_.add_node({
@@ -353,12 +383,17 @@ private:
? raw_node_sets_
: raw_element_sets_;
std::vector<std::int64_t>& labels = target[set.set_name];
if (assembly_scope) {
labels = set.sorted_unique_labels;
} else {
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());
labels.erase(
std::ranges::unique(labels).begin(), labels.end());
}
if (mesh_scope && set.kind == ResolvedSetKind::element) {
active_element_sets_[set.set_name] =
@@ -619,24 +654,37 @@ private:
continue;
}
for (const auto& row : record.data) {
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) {
add_error(
"abaqus.semantic.invalid_element_data",
"B31 element data requires a label and two nodes.",
record.source);
source);
continue;
}
const auto label = parse_label(row[0], record, "element");
const auto label =
parse_label(row[0], "element", source);
const auto first_label =
parse_label(row[1], record, "node");
parse_label(row[1], "node", source);
const auto second_label =
parse_label(row[2], record, "node");
parse_label(row[2], "node", source);
if (!label.has_value() || !first_label.has_value() ||
!second_label.has_value()) {
continue;
}
if (element_ids_.contains(*label)) {
add_error(
"abaqus.semantic.duplicate_element_label",
"Element label " + std::to_string(*label) +
" is defined more than once in its mesh scope.",
source);
continue;
}
const auto assignment = element_assignments_.find(*label);
if (assignment == element_assignments_.end()) {
add_error(
@@ -712,7 +760,7 @@ private:
std::vector<NodeId> resolve_node_target(
const std::string& target,
const DeckRecord& record) {
const SourceLocation& source) {
std::int64_t label = 0;
const auto parsed =
std::from_chars(target.data(), target.data() + target.size(), label);
@@ -732,46 +780,64 @@ private:
add_error(
"abaqus.semantic.missing_node_target",
"Node target '" + target + "' does not resolve.",
record.source);
source);
return {};
}
void collect_boundary(
const DeckRecord& record,
StepDefinition& step) {
for (const auto& row : record.data) {
if (row.size() < 2U) {
std::map<std::pair<std::int64_t, std::uint8_t>, double>&
prescribed_values) {
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() < 2U || row.size() > 4U || row[0].empty() ||
row[1].empty()) {
add_error(
"abaqus.semantic.invalid_boundary_data",
"*BOUNDARY requires a target and degree of freedom.",
record.source);
"*BOUNDARY requires target, first DOF, optional last DOF, "
"and optional value.",
source);
continue;
}
const auto first_dof = parse_dof(row[1], record);
const auto first_dof = parse_dof(row[1], source);
const auto last_dof = row.size() >= 3U && !row[2].empty()
? parse_dof(row[2], record)
? parse_dof(row[2], source)
: 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()) {
double value = 0.0;
if (row.size() >= 4U && !row[3].empty() &&
!parse_number(row[3], value)) {
add_error(
"abaqus.semantic.invalid_boundary_data",
"*BOUNDARY prescribed value must be finite.",
source);
continue;
}
if (!first_dof.has_value() || !last_dof.has_value()) {
continue;
}
if (*last_dof < *first_dof) {
add_error(
"abaqus.semantic.invalid_dof_range",
"*BOUNDARY degree-of-freedom range is reversed.",
record.source);
source);
continue;
}
const std::vector<NodeId> nodes =
resolve_node_target(row[0], record);
resolve_node_target(row[0], source);
for (const NodeId node : nodes) {
for (std::uint8_t dof = *first_dof; dof <= *last_dof; ++dof) {
step.prescribed_dofs.push_back({node, dof, *value});
const auto key = std::pair{node.value(), dof};
const auto [existing, inserted] =
prescribed_values.emplace(key, value);
if (!inserted && existing->second != value) {
add_error(
"abaqus.semantic.conflicting_boundary",
"A node DOF has conflicting prescribed values.",
source);
}
}
}
}
@@ -780,57 +846,150 @@ private:
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) {
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 || row[0].empty() || row[1].empty() ||
row[2].empty()) {
add_error(
"abaqus.semantic.invalid_cload_data",
"*CLOAD requires a target, degree of freedom, and value.",
record.source);
"*CLOAD requires exactly target, DOF, and magnitude.",
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()) {
const auto dof = parse_dof(row[1], source);
double value = 0.0;
if (!parse_number(row[2], value)) {
add_error(
"abaqus.semantic.invalid_cload_data",
"*CLOAD magnitude must be finite.",
source);
continue;
}
if (!dof.has_value()) {
continue;
}
const std::vector<NodeId> nodes =
resolve_node_target(row[0], record);
resolve_node_target(row[0], source);
for (const NodeId node : nodes) {
loads[node.value()][static_cast<std::size_t>(*dof - 1U)] +=
*value;
double& component =
loads[node.value()][static_cast<std::size_t>(*dof - 1U)];
const double sum = component + value;
if (!std::isfinite(sum)) {
add_error(
"abaqus.semantic.invalid_cload_data",
"Accumulated *CLOAD magnitude must remain finite.",
source);
continue;
}
component = sum;
}
}
}
void validate_static(const DeckRecord& record) {
if (record.data.empty()) {
return;
}
const SourceLocation first_source = data_source(record, 0U);
if (record.data[0].empty() || record.data[0].size() > 4U ||
std::ranges::any_of(
record.data[0],
[](const std::string& field) { return field.empty(); })) {
add_error(
"abaqus.semantic.invalid_static_data",
"*STATIC accepts one row of one through four positive values.",
first_source);
return;
}
for (const std::string& field : record.data[0]) {
double value = 0.0;
if (!parse_number(field, value) || value <= 0.0) {
add_error(
"abaqus.semantic.invalid_static_data",
"*STATIC values must be finite and positive.",
first_source);
return;
}
}
if (record.data.size() > 1U) {
add_error(
"abaqus.semantic.invalid_static_data",
"*STATIC accepts only one data row.",
data_source(record, 1U));
}
}
void collect_step() {
StepDefinition step;
std::map<std::pair<std::int64_t, std::uint8_t>, double>
prescribed_values;
std::map<std::int64_t, std::array<double, 6>> loads;
bool has_step = false;
const DeckRecord* first_step = nullptr;
std::size_t step_count = 0U;
std::size_t static_count = 0U;
for (const DeckRecord& record : deck_.global_records) {
if (record.keyword == "STEP") {
has_step = true;
++step_count;
if (first_step == nullptr) {
first_step = &record;
const std::string* name = parameter(record, "NAME");
step.name = name == nullptr ? "Step-1" : *name;
} else {
add_error(
"abaqus.semantic.step_count",
"Phase 1 accepts exactly one *STEP.",
record.source);
}
if (const std::string* nlgeom = parameter(record, "NLGEOM");
nlgeom != nullptr && uppercase_ascii(*nlgeom) != "NO") {
add_error(
"abaqus.semantic.unsupported_step_option",
"Phase 1 supports only NLGEOM=NO.",
record.source);
}
} else if (record.keyword == "STATIC") {
++static_count;
if (static_count > 1U) {
add_error(
"abaqus.semantic.invalid_static_data",
"A Phase 1 Step accepts exactly one *STATIC record.",
record.source);
}
validate_static(record);
} else if (record.keyword == "BOUNDARY") {
collect_boundary(record, step);
collect_boundary(record, prescribed_values);
} else if (record.keyword == "CLOAD") {
collect_loads(record, loads);
}
}
if (!has_step) {
if (step_count == 0U) {
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.",
"abaqus.semantic.step_count",
"Phase 1 input requires exactly one *STEP.",
source);
return;
}
if (step_count == 1U && static_count == 0U) {
add_error(
"abaqus.semantic.missing_static",
"The Phase 1 Step requires one *STATIC record.",
first_step->source);
}
for (const auto& [key, value] : prescribed_values) {
step.prescribed_dofs.push_back({NodeId{key.first}, key.second, value});
}
for (const auto& [node, values] : loads) {
step.nodal_loads.push_back({NodeId{node}, values});
}
+24 -2
View File
@@ -156,8 +156,6 @@ add_test(
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
--gtest_filter=AbaqusInputContract/*
)
# Steps 1-4 make this normative matrix pass, then remove WILL_FAIL.
set_tests_properties(AbaqusInputContract PROPERTIES WILL_FAIL TRUE)
add_test(
NAME SetResolution
@@ -237,6 +235,30 @@ add_test(
--gtest_filter=ActiveInstance.*
)
add_test(
NAME StepMapping
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
--gtest_filter=StepMapping.*
)
add_test(
NAME Boundary
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
--gtest_filter=Boundary.*
)
add_test(
NAME Cload
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
--gtest_filter=Cload.*
)
add_test(
NAME SuppliedCantilever
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
--gtest_filter=SuppliedCantilever.*
)
add_executable(fesa_fem_primitives_tests
unit/fem/beam_frame_test.cpp
unit/fem/dof_manager_test.cpp
@@ -6,6 +6,7 @@
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
@@ -64,6 +65,14 @@ bool has_diagnostic(
});
}
const fesa::Diagnostic* find_diagnostic(
const fesa::DomainBuildResult& result,
const std::string_view code) {
const auto found = std::ranges::find(
result.diagnostics, code, &fesa::Diagnostic::code);
return found == result.diagnostics.end() ? nullptr : &*found;
}
void expect_equivalent_analysis_data(
const fesa::Domain& flat,
const fesa::Domain& hierarchical) {
@@ -265,4 +274,212 @@ TEST(ActiveInstance, RejectsMultipleInstances) {
EXPECT_EQ(diagnostic->source->line, 6U);
}
TEST(StepMapping, MapsOneStaticStepAndRejectsUnsupportedConfigurations) {
const auto valid =
parse_and_map(fixture_path("valid/noop_directives.inp"));
ASSERT_TRUE(valid.domain.has_value());
EXPECT_TRUE(valid.diagnostics.empty());
EXPECT_EQ(valid.domain->step().name, "Step-1");
struct InvalidCase final {
std::string_view name;
std::string_view contents;
std::string_view code;
std::size_t line;
};
const InvalidCase invalid_cases[]{
{
"fesa-step-multiple.inp",
"*STEP, NAME=First\n"
"*STATIC\n"
"*END STEP\n"
"*STEP, NAME=Second\n"
"*STATIC\n"
"*END STEP\n",
"abaqus.semantic.step_count",
4U,
},
{
"fesa-step-nlgeom.inp",
"*STEP, NLGEOM=YES\n"
"*STATIC\n"
"*END STEP\n",
"abaqus.semantic.unsupported_step_option",
1U,
},
{
"fesa-static-invalid.inp",
"*STEP\n"
"*STATIC\n"
"1.0, -1.0\n"
"*END STEP\n",
"abaqus.semantic.invalid_static_data",
3U,
},
{
"fesa-static-extra-row.inp",
"*STEP\n"
"*STATIC\n"
"1.0\n"
"2.0\n"
"*END STEP\n",
"abaqus.semantic.invalid_static_data",
4U,
},
};
for (const InvalidCase& test_case : invalid_cases) {
const TemporaryDeck input{test_case.name, test_case.contents};
const auto result = parse_and_map(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, test_case.code);
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, test_case.line);
}
}
TEST(StepMapping, RejectsModelDataInsideStep) {
const TemporaryDeck input{
"fesa-node-inside-step.inp",
"*STEP\n"
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, "abaqus.syntax.invalid_node_scope");
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 2U);
}
TEST(StepMapping, RejectsBoundaryAfterCompletedStep) {
const TemporaryDeck input{
"fesa-boundary-after-step.inp",
"*STEP\n"
"*STATIC\n"
"*END STEP\n"
"*BOUNDARY\n"
"1, 1\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, "abaqus.syntax.invalid_boundary_scope");
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 4U);
}
TEST(Boundary, CanonicalizesIdenticalGlobalAndStepPrescriptions) {
const TemporaryDeck input{
"fesa-boundary-canonical.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*BOUNDARY\n"
"1, 1, 3, 2.5\n"
"*STEP\n"
"*STATIC\n"
"*BOUNDARY\n"
"1, 1, 3, 2.5\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
const auto& prescribed = result.domain->step().prescribed_dofs;
ASSERT_EQ(prescribed.size(), 3U);
for (std::size_t index = 0; index < prescribed.size(); ++index) {
EXPECT_EQ(prescribed[index].node, fesa::NodeId{0});
EXPECT_EQ(prescribed[index].dof, index + 1U);
EXPECT_DOUBLE_EQ(prescribed[index].value, 2.5);
}
}
TEST(Boundary, ReportsConflictAtTheConflictingDataRow) {
const auto result =
parse_and_map(fixture_path("invalid/boundary_conflict.inp"));
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, "abaqus.semantic.conflicting_boundary");
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 10U);
}
TEST(Cload, SumsForceAndMomentComponentsInInputOrder) {
const TemporaryDeck input{
"fesa-cload-sum.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*STEP, NAME=Load\n"
"*STATIC\n"
"*CLOAD\n"
"1, 1, 2.0\n"
"1, 1, -0.5\n"
"1, 6, 4.0\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
ASSERT_EQ(result.domain->step().nodal_loads.size(), 1U);
const auto& load = result.domain->step().nodal_loads.front();
EXPECT_EQ(load.node, fesa::NodeId{0});
EXPECT_EQ(
load.values,
(std::array<double, 6>{1.5, 0.0, 0.0, 0.0, 0.0, 4.0}));
}
TEST(Cload, ReportsInvalidDofAtTheDataRow) {
const auto result =
parse_and_map(fixture_path("invalid/cload_invalid_dof.inp"));
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, "abaqus.semantic.invalid_dof");
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 6U);
}
TEST(SuppliedCantilever, NormalizesReferenceModelThroughPublicParserAndMapper) {
const std::filesystem::path path =
std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() /
"reference" / "cantilever beam" / "cantilever beam.inp";
const auto result = parse_and_map(path);
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
const fesa::Domain& domain = *result.domain;
EXPECT_EQ(domain.nodes().size(), 11U);
EXPECT_EQ(domain.beam_elements().size(), 10U);
EXPECT_EQ(domain.step().prescribed_dofs.size(), 6U);
ASSERT_EQ(domain.step().nodal_loads.size(), 1U);
const auto node = std::ranges::find_if(
domain.nodes(),
[](const fesa::Node& candidate) {
return candidate.origin.local_label == 11;
});
ASSERT_NE(node, domain.nodes().end());
EXPECT_EQ(domain.step().nodal_loads.front().node, node->id);
EXPECT_EQ(
domain.step().nodal_loads.front().values,
(std::array<double, 6>{0.0, 0.0, -10000.0, 0.0, 0.0, 0.0}));
}
} // namespace