feat(abaqus-subset-completion): step 4 — step-bc-load-and-noop-directives
This commit is contained in:
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <charconv>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <initializer_list>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@@ -13,7 +16,7 @@
|
|||||||
namespace fesa {
|
namespace fesa {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
enum class Scope { global, part, assembly, instance };
|
enum class Scope { global, part, assembly, instance, step };
|
||||||
|
|
||||||
struct KeywordLine final {
|
struct KeywordLine final {
|
||||||
std::string keyword;
|
std::string keyword;
|
||||||
@@ -21,6 +24,10 @@ struct KeywordLine final {
|
|||||||
SourceLocation source;
|
SourceLocation source;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const std::string* parameter(
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
std::string_view name);
|
||||||
|
|
||||||
std::string_view trim(const std::string_view value) {
|
std::string_view trim(const std::string_view value) {
|
||||||
constexpr std::string_view whitespace{" \t\f\v\r\n"};
|
constexpr std::string_view whitespace{" \t\f\v\r\n"};
|
||||||
const std::size_t first = value.find_first_not_of(whitespace);
|
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{"ELASTIC"},
|
||||||
std::string_view{"ELEMENT"},
|
std::string_view{"ELEMENT"},
|
||||||
std::string_view{"ELSET"},
|
std::string_view{"ELSET"},
|
||||||
std::string_view{"END STEP"},
|
std::string_view{"HEADING"},
|
||||||
std::string_view{"MATERIAL"},
|
std::string_view{"MATERIAL"},
|
||||||
std::string_view{"NODE"},
|
std::string_view{"NODE"},
|
||||||
std::string_view{"NSET"},
|
std::string_view{"NSET"},
|
||||||
|
std::string_view{"OUTPUT"},
|
||||||
|
std::string_view{"PREPRINT"},
|
||||||
|
std::string_view{"RESTART"},
|
||||||
std::string_view{"STATIC"},
|
std::string_view{"STATIC"},
|
||||||
std::string_view{"STEP"},
|
|
||||||
std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
|
std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
|
||||||
};
|
};
|
||||||
return std::ranges::find(supported, keyword) != supported.end();
|
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(
|
std::optional<KeywordLine> parse_keyword_line(
|
||||||
const std::string_view line,
|
const std::string_view line,
|
||||||
const SourceLocation& source,
|
const SourceLocation& source,
|
||||||
@@ -190,6 +350,8 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
std::optional<ParsedPart> current_part;
|
std::optional<ParsedPart> current_part;
|
||||||
std::optional<ParsedAssembly> current_assembly;
|
std::optional<ParsedAssembly> current_assembly;
|
||||||
std::optional<ParsedInstance> current_instance;
|
std::optional<ParsedInstance> current_instance;
|
||||||
|
std::optional<SourceLocation> current_step_source;
|
||||||
|
bool completed_step = false;
|
||||||
DeckRecord* current_record = nullptr;
|
DeckRecord* current_record = nullptr;
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
@@ -249,6 +411,69 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
KeywordLine& keyword = *parsed;
|
KeywordLine& keyword = *parsed;
|
||||||
current_record = nullptr;
|
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 (keyword.keyword == "PART") {
|
||||||
if (scope != Scope::global) {
|
if (scope != Scope::global) {
|
||||||
return syntax_failure(
|
return syntax_failure(
|
||||||
@@ -353,12 +578,96 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
continue;
|
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)) {
|
if (!is_supported_record(keyword.keyword)) {
|
||||||
return syntax_failure(
|
return syntax_failure(
|
||||||
"abaqus.unsupported_keyword",
|
"abaqus.unsupported_keyword",
|
||||||
"Unsupported Abaqus keyword *" + keyword.keyword + ".",
|
"Unsupported Abaqus keyword *" + keyword.keyword + ".",
|
||||||
source);
|
source);
|
||||||
}
|
}
|
||||||
|
if (scope == Scope::step && keyword.keyword != "STATIC" &&
|
||||||
|
keyword.keyword != "BOUNDARY" && keyword.keyword != "CLOAD") {
|
||||||
|
return invalid_record_scope(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
DeckRecord next_record{
|
DeckRecord next_record{
|
||||||
std::move(keyword.keyword),
|
std::move(keyword.keyword),
|
||||||
@@ -385,6 +694,10 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
"abaqus.syntax.instance_local_keyword",
|
"abaqus.syntax.instance_local_keyword",
|
||||||
"Keyword records inside *INSTANCE are unsupported.",
|
"Keyword records inside *INSTANCE are unsupported.",
|
||||||
source);
|
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.",
|
"Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
|
||||||
current_assembly->source);
|
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), {}};
|
return {std::move(deck), {}};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ bool parse_number(const std::string_view text, double& value) {
|
|||||||
parsed.ptr == text.data() + text.size() && std::isfinite(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 {
|
class DeckMapper final {
|
||||||
public:
|
public:
|
||||||
explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}
|
explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}
|
||||||
@@ -141,8 +151,8 @@ private:
|
|||||||
|
|
||||||
std::optional<std::int64_t> parse_label(
|
std::optional<std::int64_t> parse_label(
|
||||||
const std::string_view text,
|
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;
|
std::int64_t value = 0;
|
||||||
const auto parsed =
|
const auto parsed =
|
||||||
std::from_chars(text.data(), text.data() + text.size(), value);
|
std::from_chars(text.data(), text.data() + text.size(), value);
|
||||||
@@ -152,28 +162,35 @@ private:
|
|||||||
"abaqus.semantic.invalid_label",
|
"abaqus.semantic.invalid_label",
|
||||||
"Invalid " + std::string{purpose} + " label '" +
|
"Invalid " + std::string{purpose} + " label '" +
|
||||||
std::string{text} + "'.",
|
std::string{text} + "'.",
|
||||||
record.source);
|
source);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return value;
|
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(
|
std::optional<std::uint8_t> parse_dof(
|
||||||
const std::string_view text,
|
const std::string_view text,
|
||||||
const DeckRecord& record) {
|
const SourceLocation& source) {
|
||||||
const std::optional<std::int64_t> value =
|
std::int64_t value = 0;
|
||||||
parse_label(text, record, "degree-of-freedom");
|
const auto parsed =
|
||||||
if (!value.has_value()) {
|
std::from_chars(text.data(), text.data() + text.size(), value);
|
||||||
return std::nullopt;
|
if (parsed.ec != std::errc{} ||
|
||||||
}
|
parsed.ptr != text.data() + text.size() || value < 1 ||
|
||||||
if (*value > 6) {
|
value > 6) {
|
||||||
add_error(
|
add_error(
|
||||||
"abaqus.semantic.invalid_dof",
|
"abaqus.semantic.invalid_dof",
|
||||||
"Phase 1 supports only degrees of freedom 1 through 6.",
|
"Phase 1 supports only degrees of freedom 1 through 6.",
|
||||||
record.source);
|
source);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return static_cast<std::uint8_t>(*value);
|
return static_cast<std::uint8_t>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<double> parse_real(
|
std::optional<double> parse_real(
|
||||||
@@ -187,7 +204,7 @@ private:
|
|||||||
value,
|
value,
|
||||||
std::chars_format::general);
|
std::chars_format::general);
|
||||||
if (parsed.ec != std::errc{} ||
|
if (parsed.ec != std::errc{} ||
|
||||||
parsed.ptr != text.data() + text.size()) {
|
parsed.ptr != text.data() + text.size() || !std::isfinite(value)) {
|
||||||
add_error(
|
add_error(
|
||||||
"abaqus.semantic.invalid_number",
|
"abaqus.semantic.invalid_number",
|
||||||
"Invalid " + std::string{purpose} + " value '" +
|
"Invalid " + std::string{purpose} + " value '" +
|
||||||
@@ -306,16 +323,20 @@ private:
|
|||||||
if (record.keyword != "NODE") {
|
if (record.keyword != "NODE") {
|
||||||
continue;
|
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) {
|
if (row.size() < 4U) {
|
||||||
add_error(
|
add_error(
|
||||||
"abaqus.semantic.invalid_node_data",
|
"abaqus.semantic.invalid_node_data",
|
||||||
"*NODE requires a label and three coordinates.",
|
"*NODE requires a label and three coordinates.",
|
||||||
record.source);
|
source);
|
||||||
continue;
|
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 x = parse_real(row[1], record, "node coordinate");
|
||||||
const auto y = parse_real(row[2], 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 z = parse_real(row[3], record, "node coordinate");
|
||||||
@@ -324,6 +345,15 @@ private:
|
|||||||
continue;
|
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_++};
|
const NodeId id{next_node_id_++};
|
||||||
node_ids_.emplace(*label, id);
|
node_ids_.emplace(*label, id);
|
||||||
builder_.add_node({
|
builder_.add_node({
|
||||||
@@ -353,12 +383,17 @@ private:
|
|||||||
? raw_node_sets_
|
? raw_node_sets_
|
||||||
: raw_element_sets_;
|
: raw_element_sets_;
|
||||||
std::vector<std::int64_t>& labels = target[set.set_name];
|
std::vector<std::int64_t>& labels = target[set.set_name];
|
||||||
labels.insert(
|
if (assembly_scope) {
|
||||||
labels.end(),
|
labels = set.sorted_unique_labels;
|
||||||
set.sorted_unique_labels.begin(),
|
} else {
|
||||||
set.sorted_unique_labels.end());
|
labels.insert(
|
||||||
std::ranges::sort(labels);
|
labels.end(),
|
||||||
labels.erase(std::ranges::unique(labels).begin(), 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());
|
||||||
|
}
|
||||||
|
|
||||||
if (mesh_scope && set.kind == ResolvedSetKind::element) {
|
if (mesh_scope && set.kind == ResolvedSetKind::element) {
|
||||||
active_element_sets_[set.set_name] =
|
active_element_sets_[set.set_name] =
|
||||||
@@ -619,24 +654,37 @@ private:
|
|||||||
continue;
|
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) {
|
if (row.size() < 3U) {
|
||||||
add_error(
|
add_error(
|
||||||
"abaqus.semantic.invalid_element_data",
|
"abaqus.semantic.invalid_element_data",
|
||||||
"B31 element data requires a label and two nodes.",
|
"B31 element data requires a label and two nodes.",
|
||||||
record.source);
|
source);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto label = parse_label(row[0], record, "element");
|
const auto label =
|
||||||
|
parse_label(row[0], "element", source);
|
||||||
const auto first_label =
|
const auto first_label =
|
||||||
parse_label(row[1], record, "node");
|
parse_label(row[1], "node", source);
|
||||||
const auto second_label =
|
const auto second_label =
|
||||||
parse_label(row[2], record, "node");
|
parse_label(row[2], "node", source);
|
||||||
if (!label.has_value() || !first_label.has_value() ||
|
if (!label.has_value() || !first_label.has_value() ||
|
||||||
!second_label.has_value()) {
|
!second_label.has_value()) {
|
||||||
continue;
|
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);
|
const auto assignment = element_assignments_.find(*label);
|
||||||
if (assignment == element_assignments_.end()) {
|
if (assignment == element_assignments_.end()) {
|
||||||
add_error(
|
add_error(
|
||||||
@@ -712,7 +760,7 @@ private:
|
|||||||
|
|
||||||
std::vector<NodeId> resolve_node_target(
|
std::vector<NodeId> resolve_node_target(
|
||||||
const std::string& target,
|
const std::string& target,
|
||||||
const DeckRecord& record) {
|
const SourceLocation& source) {
|
||||||
std::int64_t label = 0;
|
std::int64_t label = 0;
|
||||||
const auto parsed =
|
const auto parsed =
|
||||||
std::from_chars(target.data(), target.data() + target.size(), label);
|
std::from_chars(target.data(), target.data() + target.size(), label);
|
||||||
@@ -732,46 +780,64 @@ private:
|
|||||||
add_error(
|
add_error(
|
||||||
"abaqus.semantic.missing_node_target",
|
"abaqus.semantic.missing_node_target",
|
||||||
"Node target '" + target + "' does not resolve.",
|
"Node target '" + target + "' does not resolve.",
|
||||||
record.source);
|
source);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void collect_boundary(
|
void collect_boundary(
|
||||||
const DeckRecord& record,
|
const DeckRecord& record,
|
||||||
StepDefinition& step) {
|
std::map<std::pair<std::int64_t, std::uint8_t>, double>&
|
||||||
for (const auto& row : record.data) {
|
prescribed_values) {
|
||||||
if (row.size() < 2U) {
|
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(
|
add_error(
|
||||||
"abaqus.semantic.invalid_boundary_data",
|
"abaqus.semantic.invalid_boundary_data",
|
||||||
"*BOUNDARY requires a target and degree of freedom.",
|
"*BOUNDARY requires target, first DOF, optional last DOF, "
|
||||||
record.source);
|
"and optional value.",
|
||||||
|
source);
|
||||||
continue;
|
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()
|
const auto last_dof = row.size() >= 3U && !row[2].empty()
|
||||||
? parse_dof(row[2], record)
|
? parse_dof(row[2], source)
|
||||||
: first_dof;
|
: first_dof;
|
||||||
const auto value = row.size() >= 4U && !row[3].empty()
|
double value = 0.0;
|
||||||
? parse_real(
|
if (row.size() >= 4U && !row[3].empty() &&
|
||||||
row[3], record, "prescribed value")
|
!parse_number(row[3], value)) {
|
||||||
: std::optional<double>{0.0};
|
add_error(
|
||||||
if (!first_dof.has_value() || !last_dof.has_value() ||
|
"abaqus.semantic.invalid_boundary_data",
|
||||||
!value.has_value()) {
|
"*BOUNDARY prescribed value must be finite.",
|
||||||
|
source);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!first_dof.has_value() || !last_dof.has_value()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (*last_dof < *first_dof) {
|
if (*last_dof < *first_dof) {
|
||||||
add_error(
|
add_error(
|
||||||
"abaqus.semantic.invalid_dof_range",
|
"abaqus.semantic.invalid_dof_range",
|
||||||
"*BOUNDARY degree-of-freedom range is reversed.",
|
"*BOUNDARY degree-of-freedom range is reversed.",
|
||||||
record.source);
|
source);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<NodeId> nodes =
|
const std::vector<NodeId> nodes =
|
||||||
resolve_node_target(row[0], record);
|
resolve_node_target(row[0], source);
|
||||||
for (const NodeId node : nodes) {
|
for (const NodeId node : nodes) {
|
||||||
for (std::uint8_t dof = *first_dof; dof <= *last_dof; ++dof) {
|
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(
|
void collect_loads(
|
||||||
const DeckRecord& record,
|
const DeckRecord& record,
|
||||||
std::map<std::int64_t, std::array<double, 6>>& loads) {
|
std::map<std::int64_t, std::array<double, 6>>& loads) {
|
||||||
for (const auto& row : record.data) {
|
for (std::size_t row_index = 0; row_index < record.data.size();
|
||||||
if (row.size() < 3U) {
|
++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(
|
add_error(
|
||||||
"abaqus.semantic.invalid_cload_data",
|
"abaqus.semantic.invalid_cload_data",
|
||||||
"*CLOAD requires a target, degree of freedom, and value.",
|
"*CLOAD requires exactly target, DOF, and magnitude.",
|
||||||
record.source);
|
source);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto dof = parse_dof(row[1], record);
|
const auto dof = parse_dof(row[1], source);
|
||||||
const auto value = parse_real(row[2], record, "concentrated load");
|
double value = 0.0;
|
||||||
if (!dof.has_value() || !value.has_value()) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<NodeId> nodes =
|
const std::vector<NodeId> nodes =
|
||||||
resolve_node_target(row[0], record);
|
resolve_node_target(row[0], source);
|
||||||
for (const NodeId node : nodes) {
|
for (const NodeId node : nodes) {
|
||||||
loads[node.value()][static_cast<std::size_t>(*dof - 1U)] +=
|
double& component =
|
||||||
*value;
|
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() {
|
void collect_step() {
|
||||||
StepDefinition 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;
|
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) {
|
for (const DeckRecord& record : deck_.global_records) {
|
||||||
if (record.keyword == "STEP") {
|
if (record.keyword == "STEP") {
|
||||||
has_step = true;
|
++step_count;
|
||||||
const std::string* name = parameter(record, "NAME");
|
if (first_step == nullptr) {
|
||||||
step.name = name == nullptr ? "Step-1" : *name;
|
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") {
|
} else if (record.keyword == "BOUNDARY") {
|
||||||
collect_boundary(record, step);
|
collect_boundary(record, prescribed_values);
|
||||||
} else if (record.keyword == "CLOAD") {
|
} else if (record.keyword == "CLOAD") {
|
||||||
collect_loads(record, loads);
|
collect_loads(record, loads);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!has_step) {
|
if (step_count == 0U) {
|
||||||
const SourceLocation source =
|
const SourceLocation source =
|
||||||
deck_.global_records.empty()
|
deck_.global_records.empty()
|
||||||
? SourceLocation{}
|
? SourceLocation{}
|
||||||
: deck_.global_records.front().source;
|
: deck_.global_records.front().source;
|
||||||
add_error(
|
add_error(
|
||||||
"abaqus.semantic.missing_step",
|
"abaqus.semantic.step_count",
|
||||||
"Phase 1 input requires one *STEP.",
|
"Phase 1 input requires exactly one *STEP.",
|
||||||
source);
|
source);
|
||||||
return;
|
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) {
|
for (const auto& [node, values] : loads) {
|
||||||
step.nodal_loads.push_back({NodeId{node}, values});
|
step.nodal_loads.push_back({NodeId{node}, values});
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-2
@@ -156,8 +156,6 @@ add_test(
|
|||||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||||
--gtest_filter=AbaqusInputContract/*
|
--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(
|
add_test(
|
||||||
NAME SetResolution
|
NAME SetResolution
|
||||||
@@ -237,6 +235,30 @@ add_test(
|
|||||||
--gtest_filter=ActiveInstance.*
|
--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
|
add_executable(fesa_fem_primitives_tests
|
||||||
unit/fem/beam_frame_test.cpp
|
unit/fem/beam_frame_test.cpp
|
||||||
unit/fem/dof_manager_test.cpp
|
unit/fem/dof_manager_test.cpp
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <system_error>
|
#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(
|
void expect_equivalent_analysis_data(
|
||||||
const fesa::Domain& flat,
|
const fesa::Domain& flat,
|
||||||
const fesa::Domain& hierarchical) {
|
const fesa::Domain& hierarchical) {
|
||||||
@@ -265,4 +274,212 @@ TEST(ActiveInstance, RejectsMultipleInstances) {
|
|||||||
EXPECT_EQ(diagnostic->source->line, 6U);
|
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
|
} // namespace
|
||||||
|
|||||||
Reference in New Issue
Block a user