feat(domain-and-input-skeleton): step 2 — abaqus-scoped-syntax-parser
This commit is contained in:
@@ -27,6 +27,7 @@ include(cmake/FesaDependencies.cmake)
|
|||||||
|
|
||||||
add_library(fesa_core STATIC
|
add_library(fesa_core STATIC
|
||||||
src/fesa/core/version.cpp
|
src/fesa/core/version.cpp
|
||||||
|
src/fesa/io/abaqus/parser.cpp
|
||||||
src/fesa/model/domain.cpp
|
src/fesa/model/domain.cpp
|
||||||
src/fesa/model/domain_builder.cpp
|
src/fesa/model/domain_builder.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/source_location.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct DeckRecord final {
|
||||||
|
std::string keyword;
|
||||||
|
std::map<std::string, std::string, std::less<>> parameters;
|
||||||
|
std::vector<std::vector<std::string>> data;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedPart final {
|
||||||
|
std::string name;
|
||||||
|
std::vector<DeckRecord> records;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedInstance final {
|
||||||
|
std::string name;
|
||||||
|
std::string part_name;
|
||||||
|
std::vector<std::vector<std::string>> transform_data;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedAssembly final {
|
||||||
|
std::string name;
|
||||||
|
std::vector<ParsedInstance> instances;
|
||||||
|
std::vector<DeckRecord> records;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedDeck final {
|
||||||
|
std::vector<DeckRecord> global_records;
|
||||||
|
std::vector<ParsedPart> parts;
|
||||||
|
std::optional<ParsedAssembly> assembly;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/io/abaqus/deck_record.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct ParseDeckResult final {
|
||||||
|
std::optional<ParsedDeck> deck;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ParseDeckResult parse_deck(
|
||||||
|
const std::filesystem::path& path);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
+1
-1
@@ -42,4 +42,4 @@
|
|||||||
"status": "pending"
|
"status": "pending"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
#include <fesa/io/abaqus/parser.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class Scope { global, part, assembly, instance };
|
||||||
|
|
||||||
|
struct KeywordLine final {
|
||||||
|
std::string keyword;
|
||||||
|
std::map<std::string, std::string, std::less<>> parameters;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (first == std::string_view::npos) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const std::size_t last = value.find_last_not_of(whitespace);
|
||||||
|
return value.substr(first, last - first + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> split_fields(const std::string_view value) {
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
std::size_t first = 0;
|
||||||
|
while (true) {
|
||||||
|
const std::size_t comma = value.find(',', first);
|
||||||
|
const std::string_view field = comma == std::string_view::npos
|
||||||
|
? value.substr(first)
|
||||||
|
: value.substr(first, comma - first);
|
||||||
|
fields.emplace_back(trim(field));
|
||||||
|
if (comma == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
first = comma + 1;
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseDeckResult failure(
|
||||||
|
const DiagnosticStage stage,
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
std::optional<SourceLocation> source) {
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
diagnostics.push_back({
|
||||||
|
stage,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::move(source),
|
||||||
|
});
|
||||||
|
return {std::nullopt, std::move(diagnostics)};
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseDeckResult syntax_failure(
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
SourceLocation source) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::syntax,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::move(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_supported_record(const std::string_view keyword) {
|
||||||
|
constexpr std::array supported{
|
||||||
|
std::string_view{"BEAM GENERAL SECTION"},
|
||||||
|
std::string_view{"BOUNDARY"},
|
||||||
|
std::string_view{"CLOAD"},
|
||||||
|
std::string_view{"ELASTIC"},
|
||||||
|
std::string_view{"ELEMENT"},
|
||||||
|
std::string_view{"ELSET"},
|
||||||
|
std::string_view{"END STEP"},
|
||||||
|
std::string_view{"MATERIAL"},
|
||||||
|
std::string_view{"NODE"},
|
||||||
|
std::string_view{"NSET"},
|
||||||
|
std::string_view{"STATIC"},
|
||||||
|
std::string_view{"STEP"},
|
||||||
|
std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
|
||||||
|
};
|
||||||
|
return std::ranges::find(supported, keyword) != supported.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<KeywordLine> parse_keyword_line(
|
||||||
|
const std::string_view line,
|
||||||
|
const SourceLocation& source,
|
||||||
|
ParseDeckResult& error) {
|
||||||
|
const std::vector<std::string> fields = split_fields(line.substr(1));
|
||||||
|
if (fields.empty() || fields[0].empty()) {
|
||||||
|
error = syntax_failure(
|
||||||
|
"abaqus.syntax.empty_keyword",
|
||||||
|
"Abaqus keyword name is empty.",
|
||||||
|
source);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeywordLine parsed{
|
||||||
|
uppercase_ascii(fields[0]),
|
||||||
|
{},
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
for (std::size_t index = 1; index < fields.size(); ++index) {
|
||||||
|
const std::string_view field = fields[index];
|
||||||
|
if (field.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t equals = field.find('=');
|
||||||
|
const std::string_view key_text =
|
||||||
|
trim(field.substr(0, equals));
|
||||||
|
const std::string_view value_text =
|
||||||
|
equals == std::string_view::npos
|
||||||
|
? std::string_view{}
|
||||||
|
: trim(field.substr(equals + 1));
|
||||||
|
if (key_text.empty()) {
|
||||||
|
error = syntax_failure(
|
||||||
|
"abaqus.syntax.invalid_parameter",
|
||||||
|
"Abaqus keyword parameter name is empty.",
|
||||||
|
source);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string key = uppercase_ascii(std::string{key_text});
|
||||||
|
if (!parsed.parameters.emplace(key, value_text).second) {
|
||||||
|
error = syntax_failure(
|
||||||
|
"abaqus.syntax.duplicate_parameter",
|
||||||
|
"Abaqus keyword parameter '" + key +
|
||||||
|
"' is specified more than once.",
|
||||||
|
source);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string* parameter(
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
const std::string_view name) {
|
||||||
|
const auto found = keyword.parameters.find(name);
|
||||||
|
return found == keyword.parameters.end() ? nullptr : &found->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseDeckResult missing_parameter(
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
const std::string_view parameter_name) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.missing_parameter",
|
||||||
|
"Abaqus *" + keyword.keyword + " requires parameter " +
|
||||||
|
std::string{parameter_name} + ".",
|
||||||
|
keyword.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||||
|
std::ifstream input{path, std::ios::binary};
|
||||||
|
if (!input) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"abaqus.io.open_failed",
|
||||||
|
"Unable to open Abaqus input file.",
|
||||||
|
SourceLocation{path, 0U, 0U});
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsedDeck deck;
|
||||||
|
Scope scope = Scope::global;
|
||||||
|
std::optional<ParsedPart> current_part;
|
||||||
|
std::optional<ParsedAssembly> current_assembly;
|
||||||
|
std::optional<ParsedInstance> current_instance;
|
||||||
|
DeckRecord* current_record = nullptr;
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
std::size_t line_number = 0;
|
||||||
|
while (std::getline(input, line)) {
|
||||||
|
++line_number;
|
||||||
|
if (line_number == 1U && line.starts_with("\xEF\xBB\xBF")) {
|
||||||
|
line.erase(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t first_nonspace =
|
||||||
|
line.find_first_not_of(" \t\f\v\r");
|
||||||
|
if (first_nonspace == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string_view content = trim(line);
|
||||||
|
if (content.starts_with("**")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.starts_with('*')) {
|
||||||
|
const std::vector<std::string> fields = split_fields(content);
|
||||||
|
if (scope == Scope::instance) {
|
||||||
|
current_instance->transform_data.push_back(fields);
|
||||||
|
} else if (current_record != nullptr) {
|
||||||
|
current_record->data.push_back(fields);
|
||||||
|
} else {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.data_without_keyword",
|
||||||
|
"Abaqus data line has no preceding keyword record.",
|
||||||
|
SourceLocation{path, line_number, first_nonspace + 1U});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SourceLocation source{
|
||||||
|
path,
|
||||||
|
line_number,
|
||||||
|
first_nonspace + 1U,
|
||||||
|
};
|
||||||
|
ParseDeckResult keyword_error;
|
||||||
|
std::optional<KeywordLine> parsed =
|
||||||
|
parse_keyword_line(content, source, keyword_error);
|
||||||
|
if (!parsed.has_value()) {
|
||||||
|
return keyword_error;
|
||||||
|
}
|
||||||
|
KeywordLine& keyword = *parsed;
|
||||||
|
current_record = nullptr;
|
||||||
|
|
||||||
|
if (keyword.keyword == "PART") {
|
||||||
|
if (scope != Scope::global) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.invalid_part_scope",
|
||||||
|
"*PART is only valid in global input scope.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
const std::string* name = parameter(keyword, "NAME");
|
||||||
|
if (name == nullptr || name->empty()) {
|
||||||
|
return missing_parameter(keyword, "NAME");
|
||||||
|
}
|
||||||
|
current_part = ParsedPart{*name, {}, source};
|
||||||
|
scope = Scope::part;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "END PART") {
|
||||||
|
if (scope != Scope::part || !current_part.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unexpected_end_part",
|
||||||
|
"*END PART does not match an open *PART.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
deck.parts.push_back(std::move(*current_part));
|
||||||
|
current_part.reset();
|
||||||
|
scope = Scope::global;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "ASSEMBLY") {
|
||||||
|
if (scope != Scope::global) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.invalid_assembly_scope",
|
||||||
|
"*ASSEMBLY is only valid in global input scope.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
if (deck.assembly.has_value() ||
|
||||||
|
current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.multiple_assemblies",
|
||||||
|
"ParsedDeck can preserve only one *ASSEMBLY.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
const std::string* name = parameter(keyword, "NAME");
|
||||||
|
if (name == nullptr || name->empty()) {
|
||||||
|
return missing_parameter(keyword, "NAME");
|
||||||
|
}
|
||||||
|
current_assembly = ParsedAssembly{*name, {}, {}, source};
|
||||||
|
scope = Scope::assembly;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "END ASSEMBLY") {
|
||||||
|
if (scope != Scope::assembly ||
|
||||||
|
!current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unexpected_end_assembly",
|
||||||
|
"*END ASSEMBLY does not match an open *ASSEMBLY.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
deck.assembly = std::move(*current_assembly);
|
||||||
|
current_assembly.reset();
|
||||||
|
scope = Scope::global;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "INSTANCE") {
|
||||||
|
if (scope != Scope::assembly ||
|
||||||
|
!current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.invalid_instance_scope",
|
||||||
|
"*INSTANCE is only valid in an open *ASSEMBLY.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
const std::string* name = parameter(keyword, "NAME");
|
||||||
|
if (name == nullptr || name->empty()) {
|
||||||
|
return missing_parameter(keyword, "NAME");
|
||||||
|
}
|
||||||
|
const std::string* part_name = parameter(keyword, "PART");
|
||||||
|
if (part_name == nullptr || part_name->empty()) {
|
||||||
|
return missing_parameter(keyword, "PART");
|
||||||
|
}
|
||||||
|
current_instance =
|
||||||
|
ParsedInstance{*name, *part_name, {}, source};
|
||||||
|
scope = Scope::instance;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "END INSTANCE") {
|
||||||
|
if (scope != Scope::instance ||
|
||||||
|
!current_instance.has_value() ||
|
||||||
|
!current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unexpected_end_instance",
|
||||||
|
"*END INSTANCE does not match an open *INSTANCE.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
current_assembly->instances.push_back(
|
||||||
|
std::move(*current_instance));
|
||||||
|
current_instance.reset();
|
||||||
|
scope = Scope::assembly;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_supported_record(keyword.keyword)) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.unsupported_keyword",
|
||||||
|
"Unsupported Abaqus keyword *" + keyword.keyword + ".",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckRecord next_record{
|
||||||
|
std::move(keyword.keyword),
|
||||||
|
std::move(keyword.parameters),
|
||||||
|
{},
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
switch (scope) {
|
||||||
|
case Scope::global:
|
||||||
|
deck.global_records.push_back(std::move(next_record));
|
||||||
|
current_record = &deck.global_records.back();
|
||||||
|
break;
|
||||||
|
case Scope::part:
|
||||||
|
current_part->records.push_back(std::move(next_record));
|
||||||
|
current_record = ¤t_part->records.back();
|
||||||
|
break;
|
||||||
|
case Scope::assembly:
|
||||||
|
current_assembly->records.push_back(
|
||||||
|
std::move(next_record));
|
||||||
|
current_record = ¤t_assembly->records.back();
|
||||||
|
break;
|
||||||
|
case Scope::instance:
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.instance_local_keyword",
|
||||||
|
"Keyword records inside *INSTANCE are unsupported.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"abaqus.io.read_failed",
|
||||||
|
"Failed while reading Abaqus input file.",
|
||||||
|
SourceLocation{path, line_number, 0U});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_instance.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unclosed_instance",
|
||||||
|
"Abaqus *INSTANCE is not closed by *END INSTANCE.",
|
||||||
|
current_instance->source);
|
||||||
|
}
|
||||||
|
if (current_part.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unclosed_part",
|
||||||
|
"Abaqus *PART is not closed by *END PART.",
|
||||||
|
current_part->source);
|
||||||
|
}
|
||||||
|
if (current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unclosed_assembly",
|
||||||
|
"Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
|
||||||
|
current_assembly->source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {std::move(deck), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -110,3 +110,33 @@ add_test(
|
|||||||
NAME DomainValidation
|
NAME DomainValidation
|
||||||
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=*DomainValidation*
|
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=*DomainValidation*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_executable(fesa_abaqus_parser_tests
|
||||||
|
unit/io/abaqus/parser_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(fesa_abaqus_parser_tests PRIVATE cxx_std_20)
|
||||||
|
target_compile_options(fesa_abaqus_parser_tests PRIVATE /W4 /permissive- /EHsc)
|
||||||
|
target_compile_definitions(
|
||||||
|
fesa_abaqus_parser_tests
|
||||||
|
PRIVATE
|
||||||
|
FESA_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(fesa_abaqus_parser_tests
|
||||||
|
PRIVATE
|
||||||
|
fesa_core
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME AbaqusParser
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||||
|
--gtest_filter=AbaqusParser.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ScopedDeck
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||||
|
--gtest_filter=ScopedDeck.*
|
||||||
|
)
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
** Minimal flat/orphan-mesh cantilever syntax fixture.
|
||||||
|
|
||||||
|
*nOdE
|
||||||
|
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,
|
||||||
|
*eLeMeNt, TYPE=B31, elset=Beam
|
||||||
|
1, 1, 2
|
||||||
|
*NSET, NSET=Fixed
|
||||||
|
1
|
||||||
|
*ELSET, ELSET=Beam
|
||||||
|
1
|
||||||
|
*MATERIAL, NAME=Steel
|
||||||
|
*ELASTIC
|
||||||
|
210000.0, 0.3
|
||||||
|
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||||
|
1.0, 1.0, 1.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
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
** Minimal scoped cantilever syntax fixture.
|
||||||
|
*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
|
||||||
|
*NSET, NSET=Tip
|
||||||
|
2
|
||||||
|
*ELSET, ELSET=Beam
|
||||||
|
1
|
||||||
|
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||||
|
1.0, 1.0, 1.0, 1.0, 1.0
|
||||||
|
0.0, 1.0, 0.0
|
||||||
|
*END PART
|
||||||
|
|
||||||
|
*ASSEMBLY, NAME=RootAssembly
|
||||||
|
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||||
|
*END INSTANCE
|
||||||
|
*NSET, NSET=Fixed, INSTANCE=Beam-1
|
||||||
|
1
|
||||||
|
*NSET, NSET=Tip, INSTANCE=Beam-1
|
||||||
|
2
|
||||||
|
*END ASSEMBLY
|
||||||
|
|
||||||
|
*MATERIAL, NAME=Steel
|
||||||
|
*ELASTIC
|
||||||
|
210000.0, 0.3
|
||||||
|
*STEP, NAME=Load
|
||||||
|
*STATIC
|
||||||
|
*BOUNDARY
|
||||||
|
Fixed, 1, 6
|
||||||
|
*CLOAD
|
||||||
|
Tip, 2, -1.0
|
||||||
|
*END STEP
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
#include <fesa/io/abaqus/parser.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TemporaryDeck final {
|
||||||
|
public:
|
||||||
|
TemporaryDeck(std::string_view name, std::string_view contents)
|
||||||
|
: path_{std::filesystem::path{testing::TempDir()} / name} {
|
||||||
|
std::ofstream output{path_, std::ios::binary};
|
||||||
|
output.write(
|
||||||
|
contents.data(), static_cast<std::streamsize>(contents.size()));
|
||||||
|
if (!output) {
|
||||||
|
throw std::runtime_error{"Failed to write temporary Abaqus deck."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~TemporaryDeck() {
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::remove(path_, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TemporaryDeck(const TemporaryDeck&) = delete;
|
||||||
|
TemporaryDeck& operator=(const TemporaryDeck&) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
||||||
|
return path_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path fixture_path(std::string_view name) {
|
||||||
|
return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
|
||||||
|
"abaqus" / name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::DeckRecord& record(
|
||||||
|
const std::vector<fesa::DeckRecord>& records,
|
||||||
|
const std::string_view keyword) {
|
||||||
|
const auto found = std::ranges::find(
|
||||||
|
records, keyword, &fesa::DeckRecord::keyword);
|
||||||
|
if (found == records.end()) {
|
||||||
|
throw std::runtime_error{"Expected deck record was not parsed."};
|
||||||
|
}
|
||||||
|
return *found;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
|
||||||
|
const auto path = fixture_path("minimal_cantilever.inp");
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(path);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
EXPECT_TRUE(result.deck->parts.empty());
|
||||||
|
EXPECT_FALSE(result.deck->assembly.has_value());
|
||||||
|
|
||||||
|
const auto& nodes = record(result.deck->global_records, "NODE");
|
||||||
|
ASSERT_EQ(nodes.data.size(), 2U);
|
||||||
|
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", ""}));
|
||||||
|
EXPECT_EQ(nodes.source.file, path);
|
||||||
|
EXPECT_EQ(nodes.source.line, 3U);
|
||||||
|
EXPECT_EQ(nodes.source.column, 1U);
|
||||||
|
|
||||||
|
const auto& element = record(result.deck->global_records, "ELEMENT");
|
||||||
|
EXPECT_EQ(element.parameters.at("TYPE"), "B31");
|
||||||
|
EXPECT_EQ(element.parameters.at("ELSET"), "Beam");
|
||||||
|
EXPECT_EQ(element.source.line, 8U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AbaqusParser, PreservesUtf8ParameterValues) {
|
||||||
|
const std::string utf8_name{
|
||||||
|
"\xEB\xB9\x94\xEB\xB6\x80\xED\x92\x88"};
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-parser-utf8.inp",
|
||||||
|
"*MATERIAL, NAME=" + utf8_name + "\n"
|
||||||
|
"*ELASTIC\n"
|
||||||
|
"210000.0, 0.3\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(input.path());
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
const auto& material = record(result.deck->global_records, "MATERIAL");
|
||||||
|
EXPECT_EQ(material.parameters.at("NAME"), utf8_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AbaqusParser, RejectsUnsupportedKeywordsInsteadOfIgnoringThem) {
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-parser-unsupported.inp",
|
||||||
|
"** The error source must identify the keyword line.\n"
|
||||||
|
"*INCLUDE, INPUT=other.inp\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].stage, fesa::DiagnosticStage::syntax);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.diagnostics[0].code, "abaqus.unsupported_keyword");
|
||||||
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||||
|
EXPECT_EQ(result.diagnostics[0].source->file, input.path());
|
||||||
|
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedDeck, PreservesPartAssemblyAndInstanceScopes) {
|
||||||
|
const auto path =
|
||||||
|
fixture_path("minimal_part_instance_cantilever.inp");
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(path);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.deck->parts.size(), 1U);
|
||||||
|
EXPECT_EQ(result.deck->parts[0].name, "BeamPart");
|
||||||
|
EXPECT_EQ(result.deck->parts[0].source.file, path);
|
||||||
|
EXPECT_EQ(result.deck->parts[0].source.line, 2U);
|
||||||
|
EXPECT_EQ(record(result.deck->parts[0].records, "NODE").source.line, 3U);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck->assembly.has_value());
|
||||||
|
EXPECT_EQ(result.deck->assembly->name, "RootAssembly");
|
||||||
|
EXPECT_EQ(result.deck->assembly->source.line, 19U);
|
||||||
|
ASSERT_EQ(result.deck->assembly->instances.size(), 1U);
|
||||||
|
EXPECT_EQ(result.deck->assembly->instances[0].name, "Beam-1");
|
||||||
|
EXPECT_EQ(result.deck->assembly->instances[0].part_name, "BeamPart");
|
||||||
|
EXPECT_EQ(result.deck->assembly->instances[0].source.line, 20U);
|
||||||
|
EXPECT_TRUE(
|
||||||
|
result.deck->assembly->instances[0].transform_data.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
record(result.deck->assembly->records, "NSET")
|
||||||
|
.parameters.at("INSTANCE"),
|
||||||
|
"Beam-1");
|
||||||
|
|
||||||
|
EXPECT_NE(
|
||||||
|
std::ranges::find(
|
||||||
|
result.deck->global_records,
|
||||||
|
"MATERIAL",
|
||||||
|
&fesa::DeckRecord::keyword),
|
||||||
|
result.deck->global_records.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedDeck, PreservesInstanceTransformDataWithoutApplyingIt) {
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-parser-transform.inp",
|
||||||
|
"*PART, NAME=BeamPart\n"
|
||||||
|
"*END PART\n"
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||||
|
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||||
|
"1.0, 2.0, 3.0\n"
|
||||||
|
"0.0, 0.0, 1.0, 90.0\n"
|
||||||
|
"*END INSTANCE\n"
|
||||||
|
"*END ASSEMBLY\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(input.path());
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
|
ASSERT_TRUE(result.deck->assembly.has_value());
|
||||||
|
ASSERT_EQ(result.deck->assembly->instances.size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.deck->assembly->instances[0].transform_data,
|
||||||
|
(std::vector<std::vector<std::string>>{
|
||||||
|
{"1.0", "2.0", "3.0"},
|
||||||
|
{"0.0", "0.0", "1.0", "90.0"}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedDeck, ReportsUnexpectedAndUnclosedScopeTerminators) {
|
||||||
|
struct Case final {
|
||||||
|
std::string_view name;
|
||||||
|
std::string_view contents;
|
||||||
|
std::string_view code;
|
||||||
|
std::size_t line;
|
||||||
|
};
|
||||||
|
const Case cases[]{
|
||||||
|
{
|
||||||
|
"fesa-parser-unexpected-end-part.inp",
|
||||||
|
"*END PART\n",
|
||||||
|
"abaqus.syntax.unexpected_end_part",
|
||||||
|
1U,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fesa-parser-unclosed-part.inp",
|
||||||
|
"*PART, NAME=BeamPart\n"
|
||||||
|
"*NODE\n"
|
||||||
|
"1, 0.0, 0.0, 0.0\n",
|
||||||
|
"abaqus.syntax.unclosed_part",
|
||||||
|
1U,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fesa-parser-unexpected-end-instance.inp",
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||||
|
"*END INSTANCE\n",
|
||||||
|
"abaqus.syntax.unexpected_end_instance",
|
||||||
|
2U,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fesa-parser-unclosed-assembly.inp",
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n",
|
||||||
|
"abaqus.syntax.unclosed_assembly",
|
||||||
|
1U,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& test_case : cases) {
|
||||||
|
const TemporaryDeck input{test_case.name, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user