feat(abaqus-subset-completion): step 1 — part-and-assembly-set-resolution

This commit is contained in:
KOKO\Mimi
2026-08-01 02:37:05 +09:00
parent 91041f28c9
commit 3588aa2bb6
7 changed files with 796 additions and 0 deletions
+19
View File
@@ -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