#include "fesa/fem/dof_manager.h" #include #include #include #include #include #include #include #include #include #include namespace fesa { class DofManagerTestAccess { public: static void DuplicateOwnership(DofManager& dofs) { dofs.constrained_dofs_[3U] = dofs.free_dofs_[7U]; } static void RemoveFullMapping(DofManager& dofs) { dofs.free_equations_.pop_back(); } static void ReverseFreeMapping(DofManager& dofs) { std::swap(dofs.free_dofs_[0U], dofs.free_dofs_[1U]); } static void ReverseConstrainedMapping(DofManager& dofs) { std::swap(dofs.constrained_dofs_[0U], dofs.constrained_dofs_[1U]); } static void CorruptEquationMapping(DofManager& dofs) { dofs.free_equations_[dofs.free_dofs_[0U]] = dofs.free_dofs_.size(); } static void RemoveSparsePatternEntry(DofManager& dofs) { const std::size_t position = dofs.sparse_pattern_.row_offsets[1U] - 1U; dofs.sparse_pattern_.column_indices.erase( dofs.sparse_pattern_.column_indices.begin() + static_cast(position)); for (std::size_t row = 1U; row < dofs.sparse_pattern_.row_offsets.size(); ++row) { --dofs.sparse_pattern_.row_offsets[row]; } } static void AddSparsePatternEntry(DofManager& dofs) { const std::size_t position = dofs.sparse_pattern_.row_offsets[1U]; dofs.sparse_pattern_.column_indices.insert( dofs.sparse_pattern_.column_indices.begin() + static_cast(position), 12U); for (std::size_t row = 1U; row < dofs.sparse_pattern_.row_offsets.size(); ++row) { ++dofs.sparse_pattern_.row_offsets[row]; } } }; } // namespace fesa namespace { class GenericLayoutElement final : public fesa::Element { public: explicit GenericLayoutElement(fesa::ElementDofLayout layout) : layout_{std::move(layout)} {} const fesa::ElementDofLayout& DofLayout() const noexcept override { return layout_; } fesa::Result ComputeStiffness() const override { const std::size_t local_dof_count = layout_.node_indices.size() * layout_.components_per_node.size(); return fesa::Result::Success( {layout_, fesa::Matrix{local_dof_count, local_dof_count}}); } fesa::Result Recover( const fesa::Vector&) const override { return fesa::Result::Success( {layout_.source_id, fesa::BeamElementResultRows{}}); } private: fesa::ElementDofLayout layout_; }; fesa::ModelDefinition MakeDefinition() { const std::filesystem::path source{"models/dof-manager.inp"}; fesa::ModelDefinition definition{}; definition.source_path = source; definition.source_content_identity = "fnv1a64:0123456789abcdef"; definition.nodes = {{{"Beam-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 10U}}, {{"Beam-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 11U}}, {{"Beam-1", 30, "30"}, {2.0, 0.0, 0.0}, {source, 12U}}}; definition.materials = {{"Material", 1000.0, 0.25, {source, 20U}}}; definition.sections = { {"Section", 1.0, 1.0, 0.0, 1.0, 1.0, {0.0, 1.0, 0.0}, {}, {source, 30U}}}; definition.elements = { {{"Beam-1", 100, "100"}, {0U, 1U}, 0U, 0U, {source, 40U}}, {{"Beam-1", 200, "200"}, {1U, 2U}, 0U, 0U, {source, 41U}}}; definition.node_sets = { {"Root", std::optional{"Beam-1"}, {0U}, {source, 50U}}, {"Ends", std::optional{"Beam-1"}, {0U, 2U}, {source, 51U}}}; definition.steps = {{"Step-1", {{"Root", 1, 2, 0.0, {source, 60U}}, {"ends", 3, 3, 0.25, {source, 61U}}, {"20", 6, 6, -0.5, {source, 62U}}, {"Root", 1, 1, 0.0, {source, 63U}}}, {}, 0.1, 1.0, 0.01, 1.0, {source, 59U}}}; return definition; } fesa::ModelDefinition MakeShellDefinition() { const std::filesystem::path source{"models/shell-dof-manager.inp"}; fesa::ModelDefinition definition{}; definition.source_path = source; definition.source_content_identity = "fnv1a64:1122334455667788"; definition.nodes = {{{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 10U}}, {{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 11U}}, {{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 12U}}, {{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 13U}}}; definition.materials = {{"Material", 1000.0, 0.25, {source, 20U}}}; definition.shell_sections = {{"ShellSection", 0.1, 0U, {source, 30U}}}; definition.shell_elements = {{{"Shell-1", 100, "100"}, fesa::ShellSourceElementType::kS4, {2U, 0U, 3U, 1U}, 0U, 0U, {source, 40U}}}; definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; return definition; } struct DofFixture { fesa::DofManager dofs; }; DofFixture MakeDofFixture(fesa::ModelDefinition definition = MakeDefinition()) { auto domain = fesa::Domain::Create(std::move(definition)); EXPECT_TRUE(domain.HasValue()); auto model = fesa::AnalysisModel::Create(domain.Value()); EXPECT_TRUE(model.HasValue()); auto dofs = fesa::DofManager::Create(model.Value()); EXPECT_TRUE(dofs.HasValue()); return {std::move(dofs.Value())}; } std::vector RowColumns(const fesa::SparsePattern& pattern, std::size_t row) { return {pattern.column_indices.begin() + pattern.row_offsets[row], pattern.column_indices.begin() + pattern.row_offsets[row + 1U]}; } void ExpectInvariantFailure(const fesa::DofManager& dofs, const std::string& code) { const fesa::Status status = dofs.ValidateInvariants(); ASSERT_FALSE(status.IsOk()); EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0U].code, code); } } // namespace // C-DOF-001 TEST(DofManager, BuildsGenericRuntimeLayoutsInDeclaredSourceOrder) { auto domain = fesa::Domain::Create(MakeDefinition()); ASSERT_TRUE(domain.HasValue()); auto model = fesa::AnalysisModel::Create(domain.Value()); ASSERT_TRUE(model.HasValue()); GenericLayoutElement first{fesa::ElementDofLayout{ {"Beam-1", 100, "100"}, {2U, 0U}, {fesa::DofComponent::kUz, fesa::DofComponent::kUx}}}; GenericLayoutElement second{ fesa::ElementDofLayout{{"Beam-1", 200, "200"}, {1U}, {fesa::DofComponent::kUrz, fesa::DofComponent::kUy, fesa::DofComponent::kUrx}}}; const fesa::ElementView elements{std::cref(first), std::cref(second)}; fesa::DofManager dofs; ASSERT_TRUE(dofs.Build(model.Value(), elements).IsOk()); ASSERT_TRUE(dofs.ValidateInvariants().IsOk()); auto first_scatter = dofs.ElementScatter(first.DofLayout()); auto second_scatter = dofs.ElementScatter(second.DofLayout()); ASSERT_TRUE(first_scatter.HasValue()); ASSERT_TRUE(second_scatter.HasValue()); EXPECT_EQ(first_scatter.Value(), (std::vector{14U, 12U, 2U, 0U})); EXPECT_EQ(second_scatter.Value(), (std::vector{11U, 7U, 9U})); const auto& pattern = dofs.GetSparsePattern(); EXPECT_EQ(RowColumns(pattern, 14U), (std::vector{0U, 2U, 12U, 14U})); EXPECT_EQ(RowColumns(pattern, 7U), (std::vector{7U, 9U, 11U})); const auto expected_free_dofs = dofs.FreeDofs(); const auto expected_constrained_dofs = dofs.ConstrainedDofs(); const auto expected_row_offsets = pattern.row_offsets; const auto expected_columns = pattern.column_indices; const fesa::ElementView reversed_elements{std::cref(second), std::cref(first)}; const fesa::Status reversed_status = dofs.Build(model.Value(), reversed_elements); ASSERT_FALSE(reversed_status.IsOk()); ASSERT_EQ(reversed_status.Diagnostics().size(), 1U); EXPECT_EQ(reversed_status.Diagnostics()[0U].code, "invalid-element-layout-order"); EXPECT_EQ(dofs.FreeDofs(), expected_free_dofs); EXPECT_EQ(dofs.ConstrainedDofs(), expected_constrained_dofs); EXPECT_EQ(dofs.GetSparsePattern().row_offsets, expected_row_offsets); EXPECT_EQ(dofs.GetSparsePattern().column_indices, expected_columns); } TEST(DofManager, RejectsEveryCorruptedOwnerMappingInvariant) { const auto fixture = MakeDofFixture(); auto duplicate = fixture.dofs; fesa::DofManagerTestAccess::DuplicateOwnership(duplicate); ExpectInvariantFailure(duplicate, "duplicate-dof-mapping"); auto full = fixture.dofs; fesa::DofManagerTestAccess::RemoveFullMapping(full); ExpectInvariantFailure(full, "invalid-dof-dimensions"); auto free = fixture.dofs; fesa::DofManagerTestAccess::ReverseFreeMapping(free); ExpectInvariantFailure(free, "invalid-free-dof-mapping"); auto constrained = fixture.dofs; fesa::DofManagerTestAccess::ReverseConstrainedMapping(constrained); ExpectInvariantFailure(constrained, "invalid-constrained-dof-mapping"); auto equation = fixture.dofs; fesa::DofManagerTestAccess::CorruptEquationMapping(equation); ExpectInvariantFailure(equation, "invalid-equation-mapping"); auto missing_pattern_entry = fixture.dofs; fesa::DofManagerTestAccess::RemoveSparsePatternEntry(missing_pattern_entry); ExpectInvariantFailure(missing_pattern_entry, "invalid-dof-sparse-pattern"); auto extra_pattern_entry = fixture.dofs; fesa::DofManagerTestAccess::AddSparsePatternEntry(extra_pattern_entry); ExpectInvariantFailure(extra_pattern_entry, "invalid-dof-sparse-pattern"); } TEST(DofManager, NumbersSixDofsAndFreeEquationsStably) { const auto fixture = MakeDofFixture(); const auto& dofs = fixture.dofs; EXPECT_EQ(dofs.FullDofCount(), 18U); EXPECT_EQ(dofs.FreeDofCount(), 13U); EXPECT_EQ(dofs.ConstrainedDofCount(), 5U); EXPECT_EQ(dofs.FullDof(0U, fesa::DofComponent::kUx), 0U); EXPECT_EQ(dofs.FullDof(0U, fesa::DofComponent::kUrz), 5U); EXPECT_EQ(dofs.FullDof(1U, fesa::DofComponent::kUx), 6U); EXPECT_EQ(dofs.FullDof(2U, fesa::DofComponent::kUrz), 17U); EXPECT_EQ(dofs.FreeDofs(), (std::vector{3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 12U, 13U, 15U, 16U, 17U})); EXPECT_EQ(dofs.ConstrainedDofs(), (std::vector{0U, 1U, 2U, 11U, 14U})); EXPECT_EQ(dofs.FreeEquation(0U), std::nullopt); EXPECT_EQ(dofs.FreeEquation(3U), std::optional{0U}); EXPECT_EQ(dofs.FreeEquation(10U), std::optional{7U}); EXPECT_EQ(dofs.FreeEquation(17U), std::optional{12U}); } TEST(DofManager, ExpandsAndValidatesPrescribedValues) { const auto fixture = MakeDofFixture(); const auto& values = fixture.dofs.PrescribedValues(); ASSERT_EQ(values.Size(), 5U); EXPECT_DOUBLE_EQ(values[0], 0.0); EXPECT_DOUBLE_EQ(values[1], 0.0); EXPECT_DOUBLE_EQ(values[2], 0.25); EXPECT_DOUBLE_EQ(values[3], -0.5); EXPECT_DOUBLE_EQ(values[4], 0.25); auto conflicting_definition = MakeDefinition(); conflicting_definition.steps[0].boundaries.push_back( {"root", 1, 1, 1.0, {conflicting_definition.source_path, 77U}}); auto domain = fesa::Domain::Create(std::move(conflicting_definition)); ASSERT_TRUE(domain.HasValue()); auto model = fesa::AnalysisModel::Create(domain.Value()); ASSERT_TRUE(model.HasValue()); auto conflict = fesa::DofManager::Create(model.Value()); ASSERT_FALSE(conflict.HasValue()); EXPECT_EQ(conflict.GetStatus().Category(), fesa::FailureCategory::kInput); ASSERT_EQ(conflict.GetStatus().Diagnostics().size(), 1U); const auto& diagnostic = conflict.GetStatus().Diagnostics()[0]; EXPECT_EQ(diagnostic.code, "conflicting-boundary-condition"); EXPECT_EQ(diagnostic.keyword, "BOUNDARY"); EXPECT_EQ(diagnostic.entity_identity, "root"); EXPECT_EQ(diagnostic.location.file, std::filesystem::path{"models/dof-manager.inp"}); EXPECT_EQ(diagnostic.location.line, 77U); } TEST(DofManager, BuildsTwelveDofScatterAndSortedUniquePattern) { const auto fixture = MakeDofFixture(); const auto& dofs = fixture.dofs; EXPECT_EQ(dofs.ElementScatter(0U), (std::array{0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U})); EXPECT_EQ(dofs.ElementScatter(1U), (std::array{6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U})); const auto& pattern = dofs.GetSparsePattern(); EXPECT_EQ(pattern.row_offsets, (std::vector{0U, 12U, 24U, 36U, 48U, 60U, 72U, 90U, 108U, 126U, 144U, 162U, 180U, 192U, 204U, 216U, 228U, 240U, 252U})); const std::vector first_block{0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U}; const std::vector shared_block{0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U}; const std::vector last_block{6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U}; EXPECT_EQ(RowColumns(pattern, 0U), first_block); EXPECT_EQ(RowColumns(pattern, 7U), shared_block); EXPECT_EQ(RowColumns(pattern, 17U), last_block); for (std::size_t row = 0U; row < dofs.FullDofCount(); ++row) { const auto columns = RowColumns(pattern, row); EXPECT_TRUE(std::is_sorted(columns.begin(), columns.end())); EXPECT_EQ(std::adjacent_find(columns.begin(), columns.end()), columns.end()); } } // MITC4-DOF-001 TEST(DofManager, BuildsShellScatterInSourceNodeAndComponentOrder) { const auto fixture = MakeDofFixture(MakeShellDefinition()); const auto& dofs = fixture.dofs; EXPECT_EQ(dofs.FullDofCount(), 24U); EXPECT_EQ(dofs.ShellElementScatter(0U), (std::array{ 12U, 13U, 14U, 15U, 16U, 17U, 0U, 1U, 2U, 3U, 4U, 5U, 18U, 19U, 20U, 21U, 22U, 23U, 6U, 7U, 8U, 9U, 10U, 11U})); } // MITC4-DOF-002 TEST(DofManager, IncludesShellConnectivityInSortedUniqueSparsePattern) { const auto fixture = MakeDofFixture(MakeShellDefinition()); const auto& dofs = fixture.dofs; const auto& pattern = dofs.GetSparsePattern(); ASSERT_EQ(pattern.row_offsets.size(), 25U); ASSERT_EQ(pattern.column_indices.size(), 24U * 24U); for (std::size_t row = 0U; row < dofs.FullDofCount(); ++row) { EXPECT_EQ(pattern.row_offsets[row], row * 24U); EXPECT_EQ(pattern.row_offsets[row + 1U], (row + 1U) * 24U); const auto columns = RowColumns(pattern, row); ASSERT_EQ(columns.size(), 24U); for (std::size_t column = 0U; column < columns.size(); ++column) { EXPECT_EQ(columns[column], column); } EXPECT_TRUE(std::binary_search(columns.begin(), columns.end(), row)); EXPECT_EQ(std::adjacent_find(columns.begin(), columns.end()), columns.end()); } } TEST(DofManager, ReconstructsFullReducedRoundTrip) { const auto fixture = MakeDofFixture(); const auto& dofs = fixture.dofs; fesa::Vector full{dofs.FullDofCount()}; for (std::size_t index = 0U; index < full.Size(); ++index) { full[index] = static_cast(index) + 0.5; } for (std::size_t index = 0U; index < dofs.ConstrainedDofCount(); ++index) { full[dofs.ConstrainedDofs()[index]] = dofs.PrescribedValues()[index]; } fesa::Vector reduced{dofs.FreeDofCount()}; for (std::size_t equation = 0U; equation < reduced.Size(); ++equation) { reduced[equation] = full[dofs.FreeDofs()[equation]]; } fesa::Vector reconstructed{dofs.FullDofCount()}; for (std::size_t equation = 0U; equation < reduced.Size(); ++equation) { reconstructed[dofs.FreeDofs()[equation]] = reduced[equation]; } for (std::size_t index = 0U; index < dofs.ConstrainedDofCount(); ++index) { reconstructed[dofs.ConstrainedDofs()[index]] = dofs.PrescribedValues()[index]; } ASSERT_EQ(reconstructed.Size(), full.Size()); for (std::size_t index = 0U; index < full.Size(); ++index) { EXPECT_DOUBLE_EQ(reconstructed[index], full[index]); } }