#include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { fesa::IsotropicElastic unit_material() { return { fesa::MaterialId{0}, "Unit", 1.0, 0.25, }; } fesa::BeamSection unit_section() { return { fesa::SectionId{0}, "Unit", 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, fesa::ShearPropertySource::input, fesa::Vec3{0.0, 1.0, 0.0}, {}, }; } fesa::Domain finish_domain( fesa::DomainBuilder builder, fesa::StepDefinition step) { builder.set_step(std::move(step)); auto result = std::move(builder).build(); if (!result.domain.has_value()) { throw std::runtime_error{"Test Domain failed validation."}; } return std::move(*result.domain); } fesa::Domain build_chain_domain(const bool reverse_storage_order) { fesa::DomainBuilder builder; std::array nodes{{ { fesa::NodeId{20}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, fesa::Vec3{2.0, 0.0, 0.0}, }, { fesa::NodeId{4}, fesa::EntityOrigin{"BeamPart", "Beam-1", 100}, fesa::Vec3{0.0, 0.0, 0.0}, }, { fesa::NodeId{10}, fesa::EntityOrigin{"BeamPart", "Beam-1", 50}, fesa::Vec3{1.0, 0.0, 0.0}, }, }}; if (reverse_storage_order) { std::ranges::reverse(nodes); } for (fesa::Node& node : nodes) { builder.add_node(std::move(node)); } builder.add_material(unit_material()); builder.add_section(unit_section()); std::array elements{{ { fesa::ElementId{1}, fesa::EntityOrigin{"BeamPart", "Beam-1", 200}, {fesa::NodeId{10}, fesa::NodeId{20}}, fesa::MaterialId{0}, fesa::SectionId{0}, }, { fesa::ElementId{0}, fesa::EntityOrigin{"BeamPart", "Beam-1", 10}, {fesa::NodeId{4}, fesa::NodeId{10}}, fesa::MaterialId{0}, fesa::SectionId{0}, }, }}; if (reverse_storage_order) { std::ranges::reverse(elements); } for (fesa::BeamElement& element : elements) { builder.add_beam_element(std::move(element)); } return finish_domain( std::move(builder), { "Load", {}, { { fesa::NodeId{20}, {3.0, 4.0, 5.0, 6.0, 7.0, 8.0}, }, { fesa::NodeId{10}, {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, }, { fesa::NodeId{10}, {-1.0, 10.0, -3.0, 0.0, 0.0, 0.0}, }, }, }); } fesa::Domain build_parallel_domain( const std::vector& element_labels) { fesa::DomainBuilder builder; builder.add_node({ fesa::NodeId{4}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, fesa::Vec3{0.0, 0.0, 0.0}, }); builder.add_node({ fesa::NodeId{10}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, fesa::Vec3{1.0, 0.0, 0.0}, }); builder.add_material(unit_material()); builder.add_section(unit_section()); for (std::size_t index = 0; index < element_labels.size(); ++index) { builder.add_beam_element({ fesa::ElementId{static_cast(index)}, fesa::EntityOrigin{ "BeamPart", "Beam-1", element_labels[index], }, {fesa::NodeId{4}, fesa::NodeId{10}}, fesa::MaterialId{0}, fesa::SectionId{0}, }); } return finish_domain(std::move(builder), {"Load", {}, {}}); } fesa::Domain build_rounding_domain(const bool large_element_first) { fesa::DomainBuilder builder; builder.add_node({ fesa::NodeId{4}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, fesa::Vec3{0.0, 0.0, 0.0}, }); builder.add_node({ fesa::NodeId{10}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, fesa::Vec3{1.0, 0.0, 0.0}, }); for (std::size_t index = 0; index < 3; ++index) { fesa::IsotropicElastic material = unit_material(); material.id = fesa::MaterialId{static_cast(index)}; material.name = "Material-" + std::to_string(index); material.young = index == 2 ? 1.0e16 : 1.0; builder.add_material(std::move(material)); } builder.add_section(unit_section()); std::array storage_order{0, 1, 2}; if (large_element_first) { storage_order = {2, 0, 1}; } for (const std::size_t index : storage_order) { constexpr std::array element_ids{1, 2, 0}; builder.add_beam_element({ fesa::ElementId{element_ids[index]}, fesa::EntityOrigin{ "BeamPart", "Beam-1", static_cast((index + 1) * 10), }, {fesa::NodeId{4}, fesa::NodeId{10}}, fesa::MaterialId{static_cast(index)}, fesa::SectionId{0}, }); } return finish_domain(std::move(builder), {"Load", {}, {}}); } fesa::Domain build_orphan_node_domain() { fesa::DomainBuilder builder; builder.add_node({ fesa::NodeId{0}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, fesa::Vec3{0.0, 0.0, 0.0}, }); builder.add_node({ fesa::NodeId{1}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, fesa::Vec3{1.0, 0.0, 0.0}, }); builder.add_node({ fesa::NodeId{2}, fesa::EntityOrigin{"BeamPart", "Beam-1", 3}, fesa::Vec3{2.0, 0.0, 0.0}, }); builder.add_material(unit_material()); builder.add_section(unit_section()); builder.add_beam_element({ fesa::ElementId{0}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, {fesa::NodeId{0}, fesa::NodeId{1}}, fesa::MaterialId{0}, fesa::SectionId{0}, }); return finish_domain(std::move(builder), {"Load", {}, {}}); } double csr_value( const fesa::SymmetricCsr& matrix, std::size_t row, std::size_t column) { if (column < row) { std::swap(row, column); } const auto begin = matrix.column_indices.begin() + matrix.row_offsets[row]; const auto end = matrix.column_indices.begin() + matrix.row_offsets[row + 1]; const auto found = std::lower_bound( begin, end, static_cast(column)); if (found == end || *found != static_cast(column)) { throw std::out_of_range{"CSR entry is not present."}; } return matrix.values[static_cast( std::distance(matrix.column_indices.begin(), found))]; } std::vector value_bits( const fesa::SymmetricCsr& matrix) { std::vector bits; bits.reserve(matrix.values.size()); for (const double value : matrix.values) { bits.push_back(std::bit_cast(value)); } return bits; } TEST(CanonicalContribution, OrdersByCoordinateElementAndLocalOrder) { const std::vector contributions{ {1, 2, fesa::ElementId{9}, 4, 9.0}, {0, 2, fesa::ElementId{3}, 8, 3.8}, {0, 2, fesa::ElementId{3}, 2, 3.2}, {0, 2, fesa::ElementId{1}, 7, 1.7}, {0, 0, fesa::ElementId{5}, 0, 5.0}, }; const std::vector canonical = fesa::canonicalize_contributions(contributions); ASSERT_EQ(canonical.size(), contributions.size()); EXPECT_EQ(canonical[0].row, 0); EXPECT_EQ(canonical[0].column, 0); EXPECT_EQ(canonical[0].element, fesa::ElementId{5}); EXPECT_EQ(canonical[1].element, fesa::ElementId{1}); EXPECT_EQ(canonical[2].element, fesa::ElementId{3}); EXPECT_EQ(canonical[2].local_order, 2); EXPECT_EQ(canonical[3].element, fesa::ElementId{3}); EXPECT_EQ(canonical[3].local_order, 8); EXPECT_EQ(canonical[4].row, 1); EXPECT_EQ(canonical[4].column, 2); } TEST(DeterministicMerge, IsBitwiseStableAcrossInputPermutations) { const std::vector contributions{ {0, 1, fesa::ElementId{2}, 0, 1.0}, {0, 1, fesa::ElementId{0}, 0, 1.0e16}, {0, 1, fesa::ElementId{1}, 0, -1.0e16}, {1, 1, fesa::ElementId{1}, 1, -0.0}, {1, 1, fesa::ElementId{0}, 1, +0.0}, }; const std::vector canonical = fesa::canonicalize_contributions(contributions); const fesa::SymmetricCsr expected = fesa::merge_contributions(2, canonical); std::array permutation{0, 1, 2, 3, 4}; do { std::vector shuffled; shuffled.reserve(contributions.size()); for (const std::size_t index : permutation) { shuffled.push_back(contributions[index]); } const std::vector shuffled_canonical = fesa::canonicalize_contributions(shuffled); const fesa::SymmetricCsr actual = fesa::merge_contributions(2, shuffled_canonical); EXPECT_EQ(actual.order, expected.order); EXPECT_EQ(actual.row_offsets, expected.row_offsets); EXPECT_EQ(actual.column_indices, expected.column_indices); EXPECT_EQ(value_bits(actual), value_bits(expected)); } while (std::ranges::next_permutation(permutation).found); EXPECT_EQ( std::bit_cast(csr_value(expected, 0, 1)), std::bit_cast(1.0)); EXPECT_EQ( std::bit_cast(csr_value(expected, 1, 1)), std::bit_cast(+0.0)); } TEST(SparsePattern, BuildsExpectedTwoElementChainStructure) { const fesa::Domain domain = build_chain_domain(false); const fesa::DofManager dofs = fesa::DofManager::build(domain); const fesa::EquationSystem system = fesa::assemble_serial(domain, dofs); EXPECT_EQ( system.stiffness.row_offsets, (std::vector{ 0, 12, 23, 33, 42, 50, 57, 69, 80, 90, 99, 107, 114, 120, 125, 129, 132, 134, 135, })); std::vector expected_columns; for (std::int32_t row = 0; row < 18; ++row) { const std::int32_t last_column = row < 6 ? 11 : 17; for (std::int32_t column = row; column <= last_column; ++column) { expected_columns.push_back(column); } } EXPECT_EQ(system.stiffness.column_indices, expected_columns); } TEST(SymmetricCsr, StoresSortedUpperTriangleWithValidOffsets) { const fesa::Domain domain = build_chain_domain(false); const fesa::DofManager dofs = fesa::DofManager::build(domain); const fesa::SymmetricCsr matrix = fesa::assemble_serial(domain, dofs).stiffness; ASSERT_EQ(matrix.order, 18); ASSERT_EQ(matrix.row_offsets.size(), matrix.order + 1); ASSERT_EQ(matrix.row_offsets.front(), 0); ASSERT_EQ( static_cast(matrix.row_offsets.back()), matrix.column_indices.size()); ASSERT_EQ(matrix.column_indices.size(), matrix.values.size()); for (std::size_t row = 0; row < matrix.order; ++row) { const auto begin = matrix.column_indices.begin() + matrix.row_offsets[row]; const auto end = matrix.column_indices.begin() + matrix.row_offsets[row + 1]; EXPECT_TRUE(std::ranges::is_sorted(begin, end)); EXPECT_EQ(std::adjacent_find(begin, end), end); for (auto entry = begin; entry != end; ++entry) { EXPECT_GE(*entry, static_cast(row)); EXPECT_LT(*entry, static_cast(matrix.order)); } } } TEST(SparsePattern, IncludesZeroDiagonalForEveryOrphanNodeDof) { const fesa::Domain domain = build_orphan_node_domain(); const fesa::SymmetricCsr matrix = fesa::assemble_serial( domain, fesa::DofManager::build(domain)) .stiffness; ASSERT_EQ(matrix.order, 18); for (std::size_t row = 0; row < matrix.order; ++row) { const auto begin = matrix.column_indices.begin() + matrix.row_offsets[row]; const auto end = matrix.column_indices.begin() + matrix.row_offsets[row + 1]; const auto diagonal = std::lower_bound( begin, end, static_cast(row)); ASSERT_NE(diagonal, end); EXPECT_EQ(*diagonal, static_cast(row)); } for (std::size_t row = 12; row < 18; ++row) { EXPECT_DOUBLE_EQ(csr_value(matrix, row, row), 0.0); } } TEST(SerialAssembly, AssemblesHandCalculatedAxialChainAndFullLoad) { const fesa::Domain domain = build_chain_domain(false); const fesa::DofManager dofs = fesa::DofManager::build(domain); const fesa::EquationSystem system = fesa::assemble_serial(domain, dofs); EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 0, 0), 1.0); EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 0, 6), -1.0); EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 6, 6), 2.0); EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 6, 12), -1.0); EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 12, 12), 1.0); EXPECT_EQ( system.force, (std::vector{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 12.0, 0.0, 4.0, 5.0, 6.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, })); } TEST(SerialAssembly, MergesDuplicateElementContributions) { const fesa::Domain single_domain = build_parallel_domain({10}); const fesa::Domain duplicate_domain = build_parallel_domain({30, 10, 20}); const fesa::EquationSystem single = fesa::assemble_serial( single_domain, fesa::DofManager::build(single_domain)); const fesa::EquationSystem duplicate = fesa::assemble_serial( duplicate_domain, fesa::DofManager::build(duplicate_domain)); EXPECT_EQ( duplicate.stiffness.row_offsets, single.stiffness.row_offsets); EXPECT_EQ( duplicate.stiffness.column_indices, single.stiffness.column_indices); ASSERT_EQ(duplicate.stiffness.values.size(), single.stiffness.values.size()); for (std::size_t index = 0; index < single.stiffness.values.size(); ++index) { const double value = single.stiffness.values[index]; EXPECT_DOUBLE_EQ( duplicate.stiffness.values[index], (value + value) + value); } } TEST(SerialAssembly, ReducesByElementOriginNotDomainStorageOrder) { const fesa::Domain origin_order_domain = build_rounding_domain(false); const fesa::Domain storage_order_domain = build_rounding_domain(true); const double origin_order_value = csr_value( fesa::assemble_serial( origin_order_domain, fesa::DofManager::build(origin_order_domain)) .stiffness, 0, 0); const double storage_order_value = csr_value( fesa::assemble_serial( storage_order_domain, fesa::DofManager::build(storage_order_domain)) .stiffness, 0, 0); const double expected = (1.0 + 1.0) + 1.0e16; EXPECT_EQ( std::bit_cast(origin_order_value), std::bit_cast(expected)); EXPECT_EQ( std::bit_cast(storage_order_value), std::bit_cast(expected)); } TEST(SerialAssembly, IsIndependentOfDomainStorageAndExternalLabelOrder) { const fesa::Domain first_domain = build_chain_domain(false); const fesa::Domain second_domain = build_chain_domain(true); const fesa::EquationSystem first = fesa::assemble_serial( first_domain, fesa::DofManager::build(first_domain)); const fesa::EquationSystem second = fesa::assemble_serial( second_domain, fesa::DofManager::build(second_domain)); EXPECT_EQ(first.stiffness.order, second.stiffness.order); EXPECT_EQ( first.stiffness.row_offsets, second.stiffness.row_offsets); EXPECT_EQ( first.stiffness.column_indices, second.stiffness.column_indices); EXPECT_EQ(first.stiffness.values, second.stiffness.values); EXPECT_EQ(first.force, second.force); } TEST(SerialAssembly, PropagatesBeamKernelFailure) { fesa::DomainBuilder builder; builder.add_node({ fesa::NodeId{0}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, fesa::Vec3{0.0, 0.0, 0.0}, }); builder.add_node({ fesa::NodeId{1}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, fesa::Vec3{1.0, 0.0, 0.0}, }); auto material = unit_material(); material.young = std::numeric_limits::max(); builder.add_material(std::move(material)); auto section = unit_section(); section.area = std::numeric_limits::max(); builder.add_section(std::move(section)); builder.add_beam_element({ fesa::ElementId{0}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, {fesa::NodeId{0}, fesa::NodeId{1}}, fesa::MaterialId{0}, fesa::SectionId{0}, }); const fesa::Domain domain = finish_domain(std::move(builder), {"Load", {}, {}}); try { static_cast(fesa::assemble_serial( domain, fesa::DofManager::build(domain))); FAIL() << "Expected a Beam kernel failure."; } catch (const std::runtime_error& error) { EXPECT_NE( std::string{error.what()}.find("model.nonfinite_value"), std::string::npos); } } } // namespace