269 lines
9.6 KiB
C++
269 lines
9.6 KiB
C++
#include <fesa/io/abaqus/parser.hpp>
|
|
#include <fesa/io/abaqus/semantic_mapper.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <stdexcept>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
|
|
#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;
|
|
}
|
|
|
|
fesa::DomainBuildResult parse_and_map(const std::filesystem::path& path) {
|
|
const auto parsed = fesa::parse_deck(path);
|
|
if (!parsed.deck.has_value()) {
|
|
return {std::nullopt, parsed.diagnostics};
|
|
}
|
|
return fesa::map_deck_to_domain(*parsed.deck);
|
|
}
|
|
|
|
bool has_diagnostic(
|
|
const fesa::DomainBuildResult& result,
|
|
const std::string_view code) {
|
|
return std::ranges::any_of(
|
|
result.diagnostics,
|
|
[code](const fesa::Diagnostic& diagnostic) {
|
|
return diagnostic.code == code;
|
|
});
|
|
}
|
|
|
|
void expect_equivalent_analysis_data(
|
|
const fesa::Domain& flat,
|
|
const fesa::Domain& hierarchical) {
|
|
ASSERT_EQ(flat.nodes().size(), hierarchical.nodes().size());
|
|
ASSERT_EQ(flat.beam_elements().size(), hierarchical.beam_elements().size());
|
|
ASSERT_EQ(flat.materials().size(), hierarchical.materials().size());
|
|
ASSERT_EQ(flat.sections().size(), hierarchical.sections().size());
|
|
|
|
for (std::size_t index = 0; index < flat.nodes().size(); ++index) {
|
|
const auto& flat_node = flat.nodes()[index];
|
|
const auto& hierarchical_node = hierarchical.nodes()[index];
|
|
EXPECT_EQ(flat_node.id, hierarchical_node.id);
|
|
EXPECT_DOUBLE_EQ(flat_node.position.x, hierarchical_node.position.x);
|
|
EXPECT_DOUBLE_EQ(flat_node.position.y, hierarchical_node.position.y);
|
|
EXPECT_DOUBLE_EQ(flat_node.position.z, hierarchical_node.position.z);
|
|
}
|
|
|
|
const auto& flat_element = flat.beam_elements().front();
|
|
const auto& hierarchical_element = hierarchical.beam_elements().front();
|
|
EXPECT_EQ(flat_element.id, hierarchical_element.id);
|
|
EXPECT_EQ(flat_element.nodes, hierarchical_element.nodes);
|
|
EXPECT_EQ(flat_element.material, hierarchical_element.material);
|
|
EXPECT_EQ(flat_element.section, hierarchical_element.section);
|
|
|
|
const auto& flat_material = flat.materials().front();
|
|
const auto& hierarchical_material = hierarchical.materials().front();
|
|
EXPECT_EQ(flat_material.id, hierarchical_material.id);
|
|
EXPECT_EQ(flat_material.name, hierarchical_material.name);
|
|
EXPECT_DOUBLE_EQ(flat_material.young, hierarchical_material.young);
|
|
EXPECT_DOUBLE_EQ(flat_material.poisson, hierarchical_material.poisson);
|
|
|
|
const auto& flat_section = flat.sections().front();
|
|
const auto& hierarchical_section = hierarchical.sections().front();
|
|
EXPECT_EQ(flat_section.id, hierarchical_section.id);
|
|
EXPECT_EQ(flat_section.name, hierarchical_section.name);
|
|
EXPECT_DOUBLE_EQ(flat_section.area, hierarchical_section.area);
|
|
EXPECT_DOUBLE_EQ(flat_section.iy, hierarchical_section.iy);
|
|
EXPECT_DOUBLE_EQ(flat_section.iz, hierarchical_section.iz);
|
|
EXPECT_DOUBLE_EQ(flat_section.torsion_j, hierarchical_section.torsion_j);
|
|
EXPECT_DOUBLE_EQ(
|
|
flat_section.shear_area_y, hierarchical_section.shear_area_y);
|
|
EXPECT_DOUBLE_EQ(
|
|
flat_section.shear_area_z, hierarchical_section.shear_area_z);
|
|
|
|
ASSERT_EQ(
|
|
flat.step().prescribed_dofs.size(),
|
|
hierarchical.step().prescribed_dofs.size());
|
|
for (std::size_t index = 0;
|
|
index < flat.step().prescribed_dofs.size();
|
|
++index) {
|
|
const auto& flat_value = flat.step().prescribed_dofs[index];
|
|
const auto& hierarchical_value =
|
|
hierarchical.step().prescribed_dofs[index];
|
|
EXPECT_EQ(flat_value.node, hierarchical_value.node);
|
|
EXPECT_EQ(flat_value.dof, hierarchical_value.dof);
|
|
EXPECT_DOUBLE_EQ(flat_value.value, hierarchical_value.value);
|
|
}
|
|
|
|
ASSERT_EQ(flat.step().nodal_loads.size(), 1U);
|
|
ASSERT_EQ(hierarchical.step().nodal_loads.size(), 1U);
|
|
EXPECT_EQ(
|
|
flat.step().nodal_loads[0].node,
|
|
hierarchical.step().nodal_loads[0].node);
|
|
EXPECT_EQ(
|
|
flat.step().nodal_loads[0].values,
|
|
hierarchical.step().nodal_loads[0].values);
|
|
}
|
|
|
|
TEST(DeckToDomain, NormalizesFlatAndSingleInstanceDecksEquivalently) {
|
|
const auto flat =
|
|
parse_and_map(fixture_path("minimal_cantilever.inp"));
|
|
const auto hierarchical =
|
|
parse_and_map(fixture_path("minimal_part_instance_cantilever.inp"));
|
|
|
|
ASSERT_TRUE(flat.domain.has_value());
|
|
ASSERT_TRUE(hierarchical.domain.has_value());
|
|
EXPECT_TRUE(flat.diagnostics.empty());
|
|
EXPECT_TRUE(hierarchical.diagnostics.empty());
|
|
expect_equivalent_analysis_data(*flat.domain, *hierarchical.domain);
|
|
|
|
for (const auto& node : flat.domain->nodes()) {
|
|
EXPECT_TRUE(node.origin.part_name.empty());
|
|
EXPECT_TRUE(node.origin.instance_name.empty());
|
|
}
|
|
for (const auto& node : hierarchical.domain->nodes()) {
|
|
EXPECT_EQ(node.origin.part_name, "BeamPart");
|
|
EXPECT_EQ(node.origin.instance_name, "Beam-1");
|
|
}
|
|
EXPECT_EQ(
|
|
hierarchical.domain->beam_elements()[0].origin,
|
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1}));
|
|
|
|
const auto& section = hierarchical.domain->sections().front();
|
|
EXPECT_DOUBLE_EQ(section.shear_area_y, 5.0 * section.area / 6.0);
|
|
EXPECT_DOUBLE_EQ(section.shear_area_z, 5.0 * section.area / 6.0);
|
|
EXPECT_EQ(
|
|
section.shear_source,
|
|
fesa::ShearPropertySource::phase1_default);
|
|
}
|
|
|
|
TEST(ActiveInstance, ExcludesPartsNotReferencedByTheInstance) {
|
|
const TemporaryDeck input{
|
|
"fesa-active-instance.inp",
|
|
"*PART, NAME=Unused\n"
|
|
"*NODE\n"
|
|
"99, 9.0, 0.0, 0.0\n"
|
|
"*END PART\n"
|
|
"*PART, NAME=BeamPart\n"
|
|
"*NODE\n"
|
|
"1, 0.0, 0.0, 0.0\n"
|
|
"2, 1.0, 0.0, 0.0\n"
|
|
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
|
|
"1, 1, 2\n"
|
|
"*ELSET, ELSET=Beam\n"
|
|
"1\n"
|
|
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
|
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
|
"0.0, 1.0, 0.0\n"
|
|
"*END PART\n"
|
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
|
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
|
"*END INSTANCE\n"
|
|
"*END ASSEMBLY\n"
|
|
"*MATERIAL, NAME=Steel\n"
|
|
"*ELASTIC\n"
|
|
"210000.0, 0.3\n"
|
|
"*STEP, NAME=Load\n"
|
|
"*STATIC\n"
|
|
"*END STEP\n"};
|
|
|
|
const auto result = parse_and_map(input.path());
|
|
|
|
ASSERT_TRUE(result.domain.has_value());
|
|
ASSERT_EQ(result.domain->nodes().size(), 2U);
|
|
EXPECT_EQ(result.domain->nodes()[0].origin.part_name, "BeamPart");
|
|
EXPECT_EQ(result.domain->nodes()[1].origin.part_name, "BeamPart");
|
|
}
|
|
|
|
TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) {
|
|
const TemporaryDeck input{
|
|
"fesa-instance-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"
|
|
"*END INSTANCE\n"
|
|
"*END ASSEMBLY\n"};
|
|
|
|
const auto result = parse_and_map(input.path());
|
|
|
|
EXPECT_FALSE(result.domain.has_value());
|
|
ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.instance_transform"));
|
|
ASSERT_FALSE(result.diagnostics.empty());
|
|
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
|
EXPECT_EQ(result.diagnostics.front().source->line, 5U);
|
|
}
|
|
|
|
TEST(ActiveInstance, RejectsMissingPartReferenceWithSourceDiagnostic) {
|
|
const TemporaryDeck input{
|
|
"fesa-missing-part.inp",
|
|
"*PART, NAME=OtherPart\n"
|
|
"*END PART\n"
|
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
|
"*INSTANCE, NAME=Beam-1, PART=MissingPart\n"
|
|
"*END INSTANCE\n"
|
|
"*END ASSEMBLY\n"};
|
|
|
|
const auto result = parse_and_map(input.path());
|
|
|
|
EXPECT_FALSE(result.domain.has_value());
|
|
ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.missing_part"));
|
|
ASSERT_FALSE(result.diagnostics.empty());
|
|
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
|
EXPECT_EQ(result.diagnostics.front().source->line, 4U);
|
|
}
|
|
|
|
TEST(ActiveInstance, RejectsMultipleInstances) {
|
|
const TemporaryDeck input{
|
|
"fesa-multiple-instances.inp",
|
|
"*PART, NAME=BeamPart\n"
|
|
"*END PART\n"
|
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
|
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
|
"*END INSTANCE\n"
|
|
"*INSTANCE, NAME=Beam-2, PART=BeamPart\n"
|
|
"*END INSTANCE\n"
|
|
"*END ASSEMBLY\n"};
|
|
|
|
const auto result = parse_and_map(input.path());
|
|
|
|
EXPECT_FALSE(result.domain.has_value());
|
|
const auto diagnostic = std::ranges::find(
|
|
result.diagnostics,
|
|
std::string_view{"abaqus.semantic.instance_count"},
|
|
&fesa::Diagnostic::code);
|
|
ASSERT_NE(diagnostic, result.diagnostics.end());
|
|
ASSERT_TRUE(diagnostic->source.has_value());
|
|
EXPECT_EQ(diagnostic->source->line, 6U);
|
|
}
|
|
|
|
} // namespace
|