Files
FESADev/tests/unit/elements/element_factory_test.cpp

347 lines
13 KiB
C++

#include "fesa/elements/element_factory.h"
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
#include "fesa/elements/element.h"
#include "fesa/elements/euler_beam_3d.h"
#include "fesa/elements/mitc4_shell.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/model/domain.h"
namespace {
fesa::ModelDefinition BeamDefinition(
std::array<fesa::EntityIndex, 2> node_indices = {0U, 1U},
fesa::EntityIndex material_index = 0U,
fesa::EntityIndex property_index = 0U) {
fesa::ModelDefinition definition{};
definition.source_path = "element-factory-beam.inp";
definition.source_content_identity = "fnv1a64:beam";
definition.nodes = {
{{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {definition.source_path, 1U}},
{{"Beam-1", 2, "2"}, {2.0, 0.0, 0.0}, {definition.source_path, 2U}}};
definition.materials = {
{"Steel", 100.0, 0.25, {definition.source_path, 10U}}};
definition.sections = {{"BeamSection",
2.0,
1.0,
0.0,
1.0,
1.0,
{0.0, 1.0, 0.0},
{},
{definition.source_path, 20U}}};
definition.elements = {{{"Beam-1", 7, "7"},
node_indices,
material_index,
property_index,
{definition.source_path, 30U}}};
return definition;
}
fesa::ModelDefinition ShellDefinition(
fesa::EntityIndex definition_material_index = 0U,
fesa::EntityIndex section_material_index = 0U) {
fesa::ModelDefinition definition{};
definition.source_path = "element-factory-shell.inp";
definition.source_content_identity = "fnv1a64:shell";
definition.nodes = {
{{"Shell-1", 1, "1"}, {-1.0, -1.0, 0.0}, {definition.source_path, 1U}},
{{"Shell-1", 2, "2"}, {1.0, -1.0, 0.0}, {definition.source_path, 2U}},
{{"Shell-1", 3, "3"}, {1.0, 1.0, 0.0}, {definition.source_path, 3U}},
{{"Shell-1", 4, "4"}, {-1.0, 1.0, 0.0}, {definition.source_path, 4U}}};
definition.materials = {
{"Steel-A", 120.0, 0.25, {definition.source_path, 10U}},
{"Steel-B", 200.0, 0.30, {definition.source_path, 11U}}};
definition.shell_sections = {{"ShellSection",
0.2,
section_material_index,
{definition.source_path, 20U}}};
definition.shell_elements = {{{"Shell-1", 9, "9"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
definition_material_index,
0U,
{definition.source_path, 30U}}};
for (fesa::EntityIndex node = 0U; node < 4U; ++node) {
definition.shell_node_initial_frames.push_back(
{node, {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}});
}
return definition;
}
fesa::Domain MakeDomain(fesa::ModelDefinition definition) {
auto candidate = fesa::Domain::Create(std::move(definition));
if (!candidate.HasValue()) {
throw std::runtime_error{"Expected a valid Domain fixture."};
}
return std::move(candidate.Value());
}
void ExpectSameMatrix(const fesa::Matrix& actual,
const fesa::Matrix& expected) {
ASSERT_EQ(actual.Rows(), expected.Rows());
ASSERT_EQ(actual.Columns(), expected.Columns());
for (std::size_t row = 0U; row < actual.Rows(); ++row) {
for (std::size_t column = 0U; column < actual.Columns(); ++column) {
EXPECT_DOUBLE_EQ(actual(row, column), expected(row, column));
}
}
}
void ExpectModelFailure(
const fesa::Result<std::unique_ptr<fesa::Element>>& candidate) {
ASSERT_FALSE(candidate.HasValue());
EXPECT_EQ(candidate.GetStatus().Category(), fesa::FailureCategory::kModel);
ASSERT_EQ(candidate.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(candidate.GetStatus().Diagnostics()[0].severity,
fesa::Severity::kError);
}
class UnknownElementDefinition final : public fesa::ElementDefinition {
public:
fesa::ElementDefinitionKind Kind() const noexcept override {
return static_cast<fesa::ElementDefinitionKind>(99);
}
const fesa::SourceEntityId& SourceId() const noexcept override {
return source_id_;
}
std::string_view SourceElementType() const noexcept override {
return "UNKNOWN";
}
const std::vector<fesa::EntityIndex>& NodeIndices() const noexcept override {
return node_indices_;
}
fesa::EntityIndex MaterialIndex() const noexcept override { return 0U; }
fesa::EntityIndex PropertyIndex() const noexcept override { return 0U; }
private:
fesa::SourceEntityId source_id_{"Unknown-1", 99, "99"};
std::vector<fesa::EntityIndex> node_indices_{0U, 1U};
};
class DestructionProbeElement final : public fesa::Element {
public:
explicit DestructionProbeElement(bool& destroyed) : destroyed_{&destroyed} {}
~DestructionProbeElement() override { *destroyed_ = true; }
const fesa::ElementDofLayout& DofLayout() const noexcept override {
return layout_;
}
fesa::Result<fesa::ElementStiffnessContribution> ComputeStiffness()
const override {
return fesa::Result<fesa::ElementStiffnessContribution>::Success(
{layout_, fesa::Matrix{0U, 0U}});
}
fesa::Result<fesa::ElementResultBundle> Recover(
const fesa::Vector&) const override {
return fesa::Result<fesa::ElementResultBundle>::Success(
{layout_.source_id, fesa::BeamElementResultRows{}});
}
private:
bool* destroyed_;
fesa::ElementDofLayout layout_{};
};
} // namespace
// C-ELEMENT-001
TEST(ElementFactory, CreatesBeamWithStableLayoutStiffnessAndRecovery) {
static_assert(std::has_virtual_destructor_v<fesa::Element>);
bool destroyed = false;
{
std::unique_ptr<fesa::Element> probe =
std::make_unique<DestructionProbeElement>(destroyed);
EXPECT_NE(probe, nullptr);
}
EXPECT_TRUE(destroyed);
const fesa::Domain domain = MakeDomain(BeamDefinition());
const fesa::ElementFactory factory{};
auto candidate = factory.Create(domain.Elements()[0U], domain);
ASSERT_TRUE(candidate.HasValue());
ASSERT_NE(candidate.Value(), nullptr);
const std::unique_ptr<fesa::Element>& element = candidate.Value();
const auto& layout = element->DofLayout();
EXPECT_EQ(layout.source_id.source_label_text, "7");
EXPECT_EQ(layout.node_indices, (std::vector<fesa::EntityIndex>{0U, 1U}));
EXPECT_EQ(layout.components_per_node,
(std::vector<fesa::DofComponent>{
fesa::DofComponent::kUx, fesa::DofComponent::kUy,
fesa::DofComponent::kUz, fesa::DofComponent::kUrx,
fesa::DofComponent::kUry, fesa::DofComponent::kUrz}));
auto stiffness = element->ComputeStiffness();
ASSERT_TRUE(stiffness.HasValue());
EXPECT_EQ(stiffness.Value().layout.node_indices, layout.node_indices);
auto direct = fesa::EulerBeam3D::Create(
domain.Nodes()[0U], domain.Nodes()[1U], domain.Sections()[0U],
domain.LinearElasticMaterials()[0U]);
ASSERT_TRUE(direct.HasValue());
ExpectSameMatrix(stiffness.Value().values, direct.Value().GlobalStiffness());
fesa::Vector displacement{12U};
displacement[6U] = 0.1;
auto result = element->Recover(displacement);
ASSERT_TRUE(result.HasValue());
EXPECT_EQ(result.Value().source_id.source_label_text, "7");
ASSERT_TRUE(std::holds_alternative<fesa::BeamElementResultRows>(
result.Value().payload));
const auto& rows =
std::get<fesa::BeamElementResultRows>(result.Value().payload);
ASSERT_EQ(rows.endpoint_rows.size(), 2U);
EXPECT_DOUBLE_EQ(rows.endpoint_rows[0U].end_action[0U], -10.0);
EXPECT_DOUBLE_EQ(rows.endpoint_rows[1U].end_action[0U], 10.0);
EXPECT_DOUBLE_EQ(rows.endpoint_rows[0U].section_resultant[0U], 10.0);
EXPECT_EQ(rows.endpoint_rows[0U].node.source_label_text, "1");
EXPECT_EQ(rows.endpoint_rows[1U].node.source_label_text, "2");
EXPECT_EQ(rows.gauss_rows.size(), 2U);
EXPECT_EQ(rows.stress_rows.size(), 2U);
}
TEST(ElementFactory, CreatesMitc4WithStableLayoutStiffnessAndRecovery) {
auto mixed_definition = ShellDefinition();
mixed_definition.sections = {{"BeamSection",
2.0,
1.0,
0.0,
1.0,
1.0,
{0.0, 1.0, 0.0},
{},
{mixed_definition.source_path, 21U}}};
mixed_definition.elements = {{{"Shell-1", 8, "8"},
{0U, 1U},
0U,
0U,
{mixed_definition.source_path, 29U}}};
const fesa::Domain domain = MakeDomain(std::move(mixed_definition));
const fesa::ElementFactory factory{};
ASSERT_EQ(domain.Elements().Size(), 2U);
auto candidate = factory.Create(domain.Elements()[1U], domain);
ASSERT_TRUE(candidate.HasValue());
ASSERT_NE(candidate.Value(), nullptr);
const std::unique_ptr<fesa::Element>& element = candidate.Value();
EXPECT_EQ(element->DofLayout().node_indices,
(std::vector<fesa::EntityIndex>{0U, 1U, 2U, 3U}));
EXPECT_EQ(element->DofLayout().components_per_node.size(), 6U);
auto stiffness = element->ComputeStiffness();
ASSERT_TRUE(stiffness.HasValue());
std::array<const fesa::Node*, 4> nodes{
&domain.Nodes()[0U], &domain.Nodes()[1U], &domain.Nodes()[2U],
&domain.Nodes()[3U]};
std::array<std::array<double, 3>, 4> directors{
std::array<double, 3>{0.0, 0.0, 1.0},
{0.0, 0.0, 1.0},
{0.0, 0.0, 1.0},
{0.0, 0.0, 1.0}};
auto direct =
fesa::Mitc4Shell::Create(nodes, directors, domain.ShellSections()[0U],
domain.LinearElasticMaterials()[0U]);
ASSERT_TRUE(direct.HasValue());
auto direct_stiffness = direct.Value().Stiffness();
ASSERT_TRUE(direct_stiffness.HasValue());
ExpectSameMatrix(stiffness.Value().values,
direct_stiffness.Value().stabilized_global24);
auto result = element->Recover(fesa::Vector{24U});
ASSERT_TRUE(result.HasValue());
ASSERT_TRUE(std::holds_alternative<fesa::ShellElementResultRows>(
result.Value().payload));
const auto& rows =
std::get<fesa::ShellElementResultRows>(result.Value().payload);
ASSERT_EQ(rows.rows.size(), 4U);
EXPECT_DOUBLE_EQ(rows.physical_strain_energy, 0.0);
EXPECT_EQ(rows.rows[0U].location, fesa::ShellMidsurfaceLocation::kGp1);
EXPECT_EQ(rows.rows[3U].location, fesa::ShellMidsurfaceLocation::kGp4);
for (const auto& row : rows.rows) {
EXPECT_EQ(row.element, fesa::EntityIndex{1U});
}
}
TEST(ElementFactory, OwnerBuildsNonOwningViewInStableElementOrder) {
const fesa::Domain beam_domain = MakeDomain(BeamDefinition());
const fesa::Domain shell_domain = MakeDomain(ShellDefinition());
const fesa::ElementFactory factory{};
auto beam = factory.Create(beam_domain.Elements()[0U], beam_domain);
auto shell = factory.Create(shell_domain.Elements()[0U], shell_domain);
ASSERT_TRUE(beam.HasValue());
ASSERT_TRUE(shell.HasValue());
std::vector<std::unique_ptr<fesa::Element>> owner;
owner.push_back(std::move(beam.Value()));
owner.push_back(std::move(shell.Value()));
fesa::ElementView view;
view.reserve(owner.size());
for (const auto& element : owner) {
view.push_back(std::cref(*element));
}
ASSERT_EQ(view.size(), 2U);
EXPECT_EQ(&view[0U].get(), owner[0U].get());
EXPECT_EQ(&view[1U].get(), owner[1U].get());
EXPECT_EQ(view[0U].get().DofLayout().source_id.source_label_text, "7");
EXPECT_EQ(view[1U].get().DofLayout().source_id.source_label_text, "9");
}
TEST(ElementFactory, RejectsUnknownAndIncompatibleDefinitions) {
const fesa::ElementFactory factory{};
const fesa::Domain beam_domain = MakeDomain(BeamDefinition());
const UnknownElementDefinition unknown{};
ExpectModelFailure(factory.Create(unknown, beam_domain));
auto property_mismatch_definition = BeamDefinition();
property_mismatch_definition.shell_sections = {
{"ShellSection",
0.2,
0U,
{property_mismatch_definition.source_path, 21U}}};
property_mismatch_definition.elements[0U].section_index = 1U;
const fesa::Domain property_mismatch =
MakeDomain(std::move(property_mismatch_definition));
ExpectModelFailure(
factory.Create(property_mismatch.Elements()[0U], property_mismatch));
const fesa::Domain material_mismatch = MakeDomain(ShellDefinition(0U, 1U));
ExpectModelFailure(
factory.Create(material_mismatch.Elements()[0U], material_mismatch));
const fesa::Domain invalid_node = MakeDomain(BeamDefinition({0U, 2U}));
ExpectModelFailure(factory.Create(invalid_node.Elements()[0U], invalid_node));
auto missing_frame_definition = ShellDefinition();
missing_frame_definition.shell_node_initial_frames.pop_back();
const fesa::Domain missing_frame =
MakeDomain(std::move(missing_frame_definition));
auto missing_frame_result =
factory.Create(missing_frame.Elements()[0U], missing_frame);
ExpectModelFailure(missing_frame_result);
EXPECT_EQ(missing_frame_result.GetStatus().Diagnostics()[0U].code,
"missing-shell-frame");
}