feat(abaqus-subset-completion): step 1 — part-and-assembly-set-resolution
This commit is contained in:
@@ -38,6 +38,7 @@ add_library(fesa_core STATIC
|
||||
src/fesa/fem/line2_shape.cpp
|
||||
src/fesa/io/abaqus/parser.cpp
|
||||
src/fesa/io/abaqus/semantic_mapper.cpp
|
||||
src/fesa/io/abaqus/set_resolver.cpp
|
||||
src/fesa/io/hdf5/writer.cpp
|
||||
src/fesa/model/domain.cpp
|
||||
src/fesa/model/domain_builder.cpp
|
||||
|
||||
@@ -15,6 +15,7 @@ struct DeckRecord final {
|
||||
std::map<std::string, std::string, std::less<>> parameters;
|
||||
std::vector<std::vector<std::string>> data;
|
||||
SourceLocation source;
|
||||
std::vector<SourceLocation> data_sources;
|
||||
};
|
||||
|
||||
struct ParsedPart final {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/io/abaqus/deck_record.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
enum class ResolvedSetScope { global, part, assembly };
|
||||
|
||||
enum class ResolvedSetKind { node, element };
|
||||
|
||||
struct ResolvedSet final {
|
||||
std::string scope_name;
|
||||
std::string set_name;
|
||||
std::vector<std::int64_t> sorted_unique_labels;
|
||||
ResolvedSetScope scope = ResolvedSetScope::global;
|
||||
ResolvedSetKind kind = ResolvedSetKind::node;
|
||||
};
|
||||
|
||||
struct SetResolutionResult final {
|
||||
std::vector<ResolvedSet> sets;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
[[nodiscard]] SetResolutionResult resolve_sets(const ParsedDeck& deck);
|
||||
|
||||
} // namespace fesa
|
||||
@@ -216,6 +216,11 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
current_instance->transform_data.push_back(fields);
|
||||
} else if (current_record != nullptr) {
|
||||
current_record->data.push_back(fields);
|
||||
current_record->data_sources.push_back(SourceLocation{
|
||||
path,
|
||||
line_number,
|
||||
first_nonspace + 1U,
|
||||
});
|
||||
} else {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.data_without_keyword",
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
#include <fesa/io/abaqus/set_resolver.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <compare>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
struct SetNamespace final {
|
||||
ResolvedSetScope scope;
|
||||
std::string scope_name;
|
||||
ResolvedSetKind kind;
|
||||
|
||||
auto operator<=>(const SetNamespace&) const = default;
|
||||
};
|
||||
|
||||
struct SetKey final {
|
||||
SetNamespace name_space;
|
||||
std::string set_name;
|
||||
|
||||
auto operator<=>(const SetKey&) const = default;
|
||||
};
|
||||
|
||||
struct RawMember final {
|
||||
std::string text;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
struct RawSet final {
|
||||
std::vector<RawMember> members;
|
||||
};
|
||||
|
||||
enum class VisitState { unvisited, visiting, resolved, failed };
|
||||
|
||||
const std::string* parameter(
|
||||
const DeckRecord& record,
|
||||
const std::string_view name) {
|
||||
const auto found = record.parameters.find(name);
|
||||
return found == record.parameters.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
bool has_parameter(
|
||||
const DeckRecord& record,
|
||||
const std::string_view name) {
|
||||
return record.parameters.contains(name);
|
||||
}
|
||||
|
||||
SourceLocation data_source(
|
||||
const DeckRecord& record,
|
||||
const std::size_t row) {
|
||||
return row < record.data_sources.size()
|
||||
? record.data_sources[row]
|
||||
: record.source;
|
||||
}
|
||||
|
||||
bool parse_positive_label(
|
||||
const std::string_view text,
|
||||
std::int64_t& value) {
|
||||
const auto parsed =
|
||||
std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return parsed.ec == std::errc{} &&
|
||||
parsed.ptr == text.data() + text.size() && value > 0;
|
||||
}
|
||||
|
||||
class SetResolver final {
|
||||
public:
|
||||
explicit SetResolver(const ParsedDeck& deck) : deck_{deck} {}
|
||||
|
||||
[[nodiscard]] SetResolutionResult resolve() {
|
||||
collect_scope(
|
||||
deck_.global_records,
|
||||
{ResolvedSetScope::global, "global", ResolvedSetKind::node});
|
||||
|
||||
for (const ParsedPart& part : deck_.parts) {
|
||||
collect_scope(
|
||||
part.records,
|
||||
{ResolvedSetScope::part,
|
||||
part.name,
|
||||
ResolvedSetKind::node});
|
||||
}
|
||||
|
||||
collect_assembly();
|
||||
|
||||
std::vector<ResolvedSet> sets;
|
||||
sets.reserve(raw_sets_.size());
|
||||
for (const auto& [key, raw_set] : raw_sets_) {
|
||||
static_cast<void>(raw_set);
|
||||
if (!resolve_set(key)) {
|
||||
continue;
|
||||
}
|
||||
sets.push_back({
|
||||
key.name_space.scope_name,
|
||||
key.set_name,
|
||||
resolved_sets_.at(key),
|
||||
key.name_space.scope,
|
||||
key.name_space.kind,
|
||||
});
|
||||
}
|
||||
|
||||
if (!diagnostics_.empty()) {
|
||||
sets.clear();
|
||||
}
|
||||
return {std::move(sets), std::move(diagnostics_)};
|
||||
}
|
||||
|
||||
private:
|
||||
void add_error(
|
||||
std::string code,
|
||||
std::string message,
|
||||
const SourceLocation& source) {
|
||||
diagnostics_.push_back({
|
||||
DiagnosticStage::semantic,
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
void collect_scope(
|
||||
const std::vector<DeckRecord>& records,
|
||||
SetNamespace name_space) {
|
||||
collect_entities(records, name_space);
|
||||
collect_set_records(records, std::move(name_space), nullptr);
|
||||
}
|
||||
|
||||
void collect_entities(
|
||||
const std::vector<DeckRecord>& records,
|
||||
const SetNamespace& base_namespace) {
|
||||
for (const DeckRecord& record : records) {
|
||||
ResolvedSetKind kind;
|
||||
if (record.keyword == "NODE") {
|
||||
kind = ResolvedSetKind::node;
|
||||
} else if (record.keyword == "ELEMENT") {
|
||||
kind = ResolvedSetKind::element;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
SetNamespace entity_namespace = base_namespace;
|
||||
entity_namespace.kind = kind;
|
||||
std::set<std::int64_t>& labels = entities_[entity_namespace];
|
||||
for (std::size_t row = 0; row < record.data.size(); ++row) {
|
||||
if (record.data[row].empty()) {
|
||||
continue;
|
||||
}
|
||||
std::int64_t label = 0;
|
||||
if (!parse_positive_label(record.data[row][0], label)) {
|
||||
continue;
|
||||
}
|
||||
labels.insert(label);
|
||||
|
||||
if (kind != ResolvedSetKind::element) {
|
||||
continue;
|
||||
}
|
||||
const std::string* set_name = parameter(record, "ELSET");
|
||||
if (set_name == nullptr || set_name->empty()) {
|
||||
continue;
|
||||
}
|
||||
raw_sets_[{entity_namespace, *set_name}].members.push_back({
|
||||
std::to_string(label),
|
||||
data_source(record, row),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_set_records(
|
||||
const std::vector<DeckRecord>& records,
|
||||
const SetNamespace& base_namespace,
|
||||
const std::string* active_instance) {
|
||||
for (const DeckRecord& record : records) {
|
||||
SetNamespace set_namespace = base_namespace;
|
||||
std::string_view name_parameter;
|
||||
if (record.keyword == "NSET") {
|
||||
set_namespace.kind = ResolvedSetKind::node;
|
||||
name_parameter = "NSET";
|
||||
} else if (record.keyword == "ELSET") {
|
||||
set_namespace.kind = ResolvedSetKind::element;
|
||||
name_parameter = "ELSET";
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string* set_name = parameter(record, name_parameter);
|
||||
if (set_name == nullptr || set_name->empty()) {
|
||||
add_error(
|
||||
"abaqus.semantic.missing_parameter",
|
||||
"*" + record.keyword + " requires parameter " +
|
||||
std::string{name_parameter} + ".",
|
||||
record.source);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (set_namespace.scope == ResolvedSetScope::assembly) {
|
||||
const std::string* instance = parameter(record, "INSTANCE");
|
||||
if (active_instance == nullptr || instance == nullptr ||
|
||||
*instance != *active_instance) {
|
||||
add_error(
|
||||
"abaqus.semantic.wrong_instance",
|
||||
"Assembly set '" + *set_name +
|
||||
"' must reference the active Instance.",
|
||||
record.source);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
RawSet& raw_set = raw_sets_[{set_namespace, *set_name}];
|
||||
if (has_parameter(record, "GENERATE")) {
|
||||
collect_generate(record, raw_set);
|
||||
} else {
|
||||
collect_explicit(record, raw_set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_explicit(
|
||||
const DeckRecord& record,
|
||||
RawSet& raw_set) {
|
||||
for (std::size_t row = 0; row < record.data.size(); ++row) {
|
||||
for (const std::string& field : record.data[row]) {
|
||||
if (field.empty()) {
|
||||
continue;
|
||||
}
|
||||
raw_set.members.push_back({field, data_source(record, row)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_generate(
|
||||
const DeckRecord& record,
|
||||
RawSet& raw_set) {
|
||||
const SourceLocation source =
|
||||
record.data.empty() ? record.source : data_source(record, 0U);
|
||||
if (record.data.size() != 1U || record.data[0].size() != 3U ||
|
||||
std::ranges::any_of(
|
||||
record.data[0],
|
||||
[](const std::string& field) { return field.empty(); })) {
|
||||
add_error(
|
||||
"abaqus.semantic.invalid_generate",
|
||||
"*" + record.keyword +
|
||||
", GENERATE requires exactly start, end, increment.",
|
||||
source);
|
||||
return;
|
||||
}
|
||||
|
||||
std::int64_t start = 0;
|
||||
std::int64_t end = 0;
|
||||
std::int64_t increment = 0;
|
||||
if (!parse_positive_label(record.data[0][0], start) ||
|
||||
!parse_positive_label(record.data[0][1], end) ||
|
||||
!parse_positive_label(record.data[0][2], increment) ||
|
||||
start > end || (end - start) % increment != 0) {
|
||||
add_error(
|
||||
"abaqus.semantic.invalid_generate",
|
||||
"Invalid *" + record.keyword + " generate range.",
|
||||
source);
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::int64_t label = start;; label += increment) {
|
||||
raw_set.members.push_back({std::to_string(label), source});
|
||||
if (label == end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_assembly() {
|
||||
if (!deck_.assembly.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ParsedAssembly& assembly = *deck_.assembly;
|
||||
const ParsedInstance* active_instance =
|
||||
assembly.instances.size() == 1U
|
||||
? &assembly.instances.front()
|
||||
: nullptr;
|
||||
SetNamespace assembly_namespace{
|
||||
ResolvedSetScope::assembly,
|
||||
assembly.name,
|
||||
ResolvedSetKind::node,
|
||||
};
|
||||
|
||||
if (active_instance != nullptr) {
|
||||
const auto part = std::ranges::find(
|
||||
deck_.parts, active_instance->part_name, &ParsedPart::name);
|
||||
if (part != deck_.parts.end()) {
|
||||
for (const ResolvedSetKind kind : {
|
||||
ResolvedSetKind::node,
|
||||
ResolvedSetKind::element}) {
|
||||
const SetNamespace part_namespace{
|
||||
ResolvedSetScope::part,
|
||||
part->name,
|
||||
kind,
|
||||
};
|
||||
SetNamespace lifted_namespace = assembly_namespace;
|
||||
lifted_namespace.kind = kind;
|
||||
const auto labels = entities_.find(part_namespace);
|
||||
if (labels != entities_.end()) {
|
||||
entities_[lifted_namespace] = labels->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string* instance_name =
|
||||
active_instance == nullptr ? nullptr : &active_instance->name;
|
||||
collect_set_records(
|
||||
assembly.records, assembly_namespace, instance_name);
|
||||
}
|
||||
|
||||
bool resolve_set(const SetKey& key) {
|
||||
VisitState& state = states_[key];
|
||||
if (state == VisitState::resolved) {
|
||||
return true;
|
||||
}
|
||||
if (state == VisitState::failed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state = VisitState::visiting;
|
||||
bool succeeded = true;
|
||||
std::vector<std::int64_t> labels;
|
||||
for (const RawMember& member : raw_sets_.at(key).members) {
|
||||
std::int64_t label = 0;
|
||||
if (parse_positive_label(member.text, label)) {
|
||||
const auto entity_namespace = entities_.find(key.name_space);
|
||||
if (entity_namespace == entities_.end() ||
|
||||
!entity_namespace->second.contains(label)) {
|
||||
add_error(
|
||||
"abaqus.semantic.missing_set_member",
|
||||
"Set '" + key.set_name +
|
||||
"' references missing entity label " +
|
||||
std::to_string(label) + ".",
|
||||
member.source);
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
labels.push_back(label);
|
||||
continue;
|
||||
}
|
||||
|
||||
const SetKey nested_key{key.name_space, member.text};
|
||||
const auto nested = raw_sets_.find(nested_key);
|
||||
if (nested == raw_sets_.end()) {
|
||||
add_error(
|
||||
"abaqus.semantic.missing_set_member",
|
||||
"Set '" + key.set_name + "' references missing set '" +
|
||||
member.text + "'.",
|
||||
member.source);
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (states_[nested_key] == VisitState::visiting) {
|
||||
add_error(
|
||||
"abaqus.semantic.set_cycle",
|
||||
"Set '" + key.set_name +
|
||||
"' closes a nested set reference cycle through '" +
|
||||
member.text + "'.",
|
||||
member.source);
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
if (!resolve_set(nested_key)) {
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
const std::vector<std::int64_t>& nested_labels =
|
||||
resolved_sets_.at(nested_key);
|
||||
labels.insert(
|
||||
labels.end(), nested_labels.begin(), nested_labels.end());
|
||||
}
|
||||
|
||||
if (!succeeded) {
|
||||
state = VisitState::failed;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ranges::sort(labels);
|
||||
labels.erase(std::ranges::unique(labels).begin(), labels.end());
|
||||
resolved_sets_[key] = std::move(labels);
|
||||
state = VisitState::resolved;
|
||||
return true;
|
||||
}
|
||||
|
||||
const ParsedDeck& deck_;
|
||||
std::map<SetNamespace, std::set<std::int64_t>> entities_;
|
||||
std::map<SetKey, RawSet> raw_sets_;
|
||||
std::map<SetKey, VisitState> states_;
|
||||
std::map<SetKey, std::vector<std::int64_t>> resolved_sets_;
|
||||
std::vector<Diagnostic> diagnostics_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SetResolutionResult resolve_sets(const ParsedDeck& deck) {
|
||||
return SetResolver{deck}.resolve();
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
@@ -120,6 +120,7 @@ add_test(
|
||||
add_executable(fesa_abaqus_parser_tests
|
||||
unit/io/abaqus/input_contract_test.cpp
|
||||
unit/io/abaqus/parser_test.cpp
|
||||
unit/io/abaqus/set_resolution_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(fesa_abaqus_parser_tests PRIVATE cxx_std_20)
|
||||
@@ -156,6 +157,24 @@ add_test(
|
||||
# Steps 1-4 make this normative matrix pass, then remove WILL_FAIL.
|
||||
set_tests_properties(AbaqusInputContract PROPERTIES WILL_FAIL TRUE)
|
||||
|
||||
add_test(
|
||||
NAME SetResolution
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=SetResolution.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME PartSet
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=PartSet.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME AssemblySet
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=AssemblySet.*
|
||||
)
|
||||
|
||||
add_executable(fesa_deck_to_domain_tests
|
||||
integration/io/minimal_deck_to_domain_test.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
#include <fesa/io/abaqus/set_resolver.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <tuple>
|
||||
#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_;
|
||||
};
|
||||
|
||||
fesa::SetResolutionResult parse_and_resolve(
|
||||
const TemporaryDeck& input) {
|
||||
const fesa::ParseDeckResult parsed = fesa::parse_deck(input.path());
|
||||
if (!parsed.deck.has_value()) {
|
||||
throw std::runtime_error{"Test deck did not parse."};
|
||||
}
|
||||
return fesa::resolve_sets(*parsed.deck);
|
||||
}
|
||||
|
||||
const fesa::ResolvedSet& find_set(
|
||||
const fesa::SetResolutionResult& result,
|
||||
const fesa::ResolvedSetScope scope,
|
||||
const std::string_view scope_name,
|
||||
const fesa::ResolvedSetKind kind,
|
||||
const std::string_view set_name) {
|
||||
const auto found = std::ranges::find_if(
|
||||
result.sets,
|
||||
[=](const fesa::ResolvedSet& set) {
|
||||
return set.scope == scope && set.scope_name == scope_name &&
|
||||
set.kind == kind && set.set_name == set_name;
|
||||
});
|
||||
if (found == result.sets.end()) {
|
||||
throw std::runtime_error{"Expected resolved set was not found."};
|
||||
}
|
||||
return *found;
|
||||
}
|
||||
|
||||
const fesa::Diagnostic& find_diagnostic(
|
||||
const fesa::SetResolutionResult& result,
|
||||
const std::string_view code,
|
||||
const std::size_t line) {
|
||||
const auto found = std::ranges::find_if(
|
||||
result.diagnostics,
|
||||
[=](const fesa::Diagnostic& diagnostic) {
|
||||
return diagnostic.code == code && diagnostic.source.has_value() &&
|
||||
diagnostic.source->line == line;
|
||||
});
|
||||
if (found == result.diagnostics.end()) {
|
||||
throw std::runtime_error{"Expected set diagnostic was not found."};
|
||||
}
|
||||
return *found;
|
||||
}
|
||||
|
||||
TEST(
|
||||
SetResolution,
|
||||
CanonicalizesExplicitGenerateNestedForwardDuplicateAndEmptySets) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-valid.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"3, 2, 0, 0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=ImplicitElements\n"
|
||||
"10, 1, 2\n"
|
||||
"20, 2, 3\n"
|
||||
"*NSET, NSET=AllNodes\n"
|
||||
"GeneratedNodes, 3, 1, 3\n"
|
||||
"*NSET, NSET=GeneratedNodes, GENERATE\n"
|
||||
"1, 3, 1\n"
|
||||
"*NSET, NSET=EmptyNodes\n"
|
||||
"*ELSET, ELSET=AllElements\n"
|
||||
"GeneratedElements, ImplicitElements, 20\n"
|
||||
"*ELSET, ELSET=GeneratedElements, GENERATE\n"
|
||||
"10, 20, 10\n"
|
||||
"*END PART\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"AllNodes")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1, 2, 3}));
|
||||
EXPECT_TRUE(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"EmptyNodes")
|
||||
.sorted_unique_labels.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::element,
|
||||
"AllElements")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{10, 20}));
|
||||
}
|
||||
|
||||
TEST(PartSet, KeepsNodeAndElementSetNamespacesSeparate) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-part-set-kind-collision.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"*ELEMENT, TYPE=B31\n"
|
||||
"1, 1, 2\n"
|
||||
"*NSET, NSET=Shared\n"
|
||||
"1\n"
|
||||
"*ELSET, ELSET=Shared\n"
|
||||
"1\n"
|
||||
"*END PART\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::element,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
}
|
||||
|
||||
TEST(AssemblySet, KeepsPartScopeSeparateAndLiftsActivePartLabels) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-assembly-set-scope-collision.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"*NSET, NSET=Shared\n"
|
||||
"1\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*NSET, NSET=Shared, INSTANCE=Beam-1\n"
|
||||
"2, 2\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
ASSERT_EQ(result.sets.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::assembly,
|
||||
"RootAssembly",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{2}));
|
||||
EXPECT_TRUE(std::ranges::is_sorted(
|
||||
result.sets,
|
||||
[](const fesa::ResolvedSet& left, const fesa::ResolvedSet& right) {
|
||||
return std::tuple{
|
||||
left.scope,
|
||||
left.scope_name,
|
||||
left.kind,
|
||||
left.set_name} <
|
||||
std::tuple{
|
||||
right.scope,
|
||||
right.scope_name,
|
||||
right.kind,
|
||||
right.set_name};
|
||||
}));
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsCycleAtTheClosingReferenceSource) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-cycle.inp",
|
||||
"*NSET, NSET=First\n"
|
||||
"Second\n"
|
||||
"*NSET, NSET=Second\n"
|
||||
"First\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
const fesa::Diagnostic& diagnostic =
|
||||
find_diagnostic(result, "abaqus.semantic.set_cycle", 4U);
|
||||
EXPECT_EQ(diagnostic.stage, fesa::DiagnosticStage::semantic);
|
||||
EXPECT_EQ(diagnostic.source->file, input.path());
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsUnknownSetAndEntityAtTheirMemberRows) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-missing.inp",
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"*NSET, NSET=MissingEntity\n"
|
||||
"2\n"
|
||||
"*NSET, NSET=MissingSet\n"
|
||||
"Unknown\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
EXPECT_EQ(result.diagnostics.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(
|
||||
result, "abaqus.semantic.missing_set_member", 4U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(
|
||||
result, "abaqus.semantic.missing_set_member", 6U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsInvalidGenerateRangesInInputOrder) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-generate-invalid.inp",
|
||||
"*NSET, NSET=Reversed, GENERATE\n"
|
||||
"3, 1, 1\n"
|
||||
"*NSET, NSET=NotDivisible, GENERATE\n"
|
||||
"1, 4, 2\n"
|
||||
"*ELSET, ELSET=WrongFieldCount, GENERATE\n"
|
||||
"1, 2\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
ASSERT_EQ(result.diagnostics.size(), 3U);
|
||||
for (std::size_t index = 0; index < result.diagnostics.size(); ++index) {
|
||||
EXPECT_EQ(
|
||||
result.diagnostics[index].code,
|
||||
"abaqus.semantic.invalid_generate");
|
||||
ASSERT_TRUE(result.diagnostics[index].source.has_value());
|
||||
}
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
||||
EXPECT_EQ(result.diagnostics[1].source->line, 4U);
|
||||
EXPECT_EQ(result.diagnostics[2].source->line, 6U);
|
||||
}
|
||||
|
||||
TEST(AssemblySet, RejectsAnInstanceOtherThanTheSingleActiveInstance) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-assembly-set-wrong-instance.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*NSET, NSET=Fixed, INSTANCE=Other\n"
|
||||
"1\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(result, "abaqus.semantic.wrong_instance", 8U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user