feat(deterministic-parallel-assembly): step 0 — canonical-contribution-order
This commit is contained in:
@@ -28,6 +28,7 @@ include(cmake/FesaDependencies.cmake)
|
|||||||
add_library(fesa_core STATIC
|
add_library(fesa_core STATIC
|
||||||
src/fesa/analysis/linear_static_analysis.cpp
|
src/fesa/analysis/linear_static_analysis.cpp
|
||||||
src/fesa/analysis/run_solver.cpp
|
src/fesa/analysis/run_solver.cpp
|
||||||
|
src/fesa/assembly/contribution.cpp
|
||||||
src/fesa/assembly/serial_assembler.cpp
|
src/fesa/assembly/serial_assembler.cpp
|
||||||
src/fesa/constraints/essential_bc.cpp
|
src/fesa/constraints/essential_bc.cpp
|
||||||
src/fesa/core/version.cpp
|
src/fesa/core/version.cpp
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/assembly/symmetric_csr.hpp>
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct MatrixContribution final {
|
||||||
|
std::size_t row;
|
||||||
|
std::size_t column;
|
||||||
|
ElementId element;
|
||||||
|
std::uint16_t local_order;
|
||||||
|
double value;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<MatrixContribution> canonicalize_contributions(
|
||||||
|
std::span<const MatrixContribution> contributions);
|
||||||
|
|
||||||
|
[[nodiscard]] SymmetricCsr merge_contributions(
|
||||||
|
std::size_t order,
|
||||||
|
std::span<const MatrixContribution> canonical);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
+1
-1
@@ -47,4 +47,4 @@
|
|||||||
"status": "pending"
|
"status": "pending"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include <fesa/assembly/contribution.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iterator>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::int32_t csr_index(const std::size_t value) {
|
||||||
|
if (value >
|
||||||
|
static_cast<std::size_t>(
|
||||||
|
std::numeric_limits<std::int32_t>::max())) {
|
||||||
|
throw std::overflow_error{
|
||||||
|
"Symmetric CSR exceeds the 32-bit index range."};
|
||||||
|
}
|
||||||
|
return static_cast<std::int32_t>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool contribution_less(
|
||||||
|
const MatrixContribution& left,
|
||||||
|
const MatrixContribution& right) {
|
||||||
|
return std::tuple{
|
||||||
|
left.row,
|
||||||
|
left.column,
|
||||||
|
left.element,
|
||||||
|
left.local_order} <
|
||||||
|
std::tuple{
|
||||||
|
right.row,
|
||||||
|
right.column,
|
||||||
|
right.element,
|
||||||
|
right.local_order};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<MatrixContribution> canonicalize_contributions(
|
||||||
|
const std::span<const MatrixContribution> contributions) {
|
||||||
|
std::vector<MatrixContribution> canonical{
|
||||||
|
contributions.begin(), contributions.end()};
|
||||||
|
std::ranges::sort(canonical, contribution_less);
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
SymmetricCsr merge_contributions(
|
||||||
|
const std::size_t order,
|
||||||
|
const std::span<const MatrixContribution> canonical) {
|
||||||
|
if (!std::ranges::is_sorted(canonical, contribution_less)) {
|
||||||
|
throw std::invalid_argument{
|
||||||
|
"Matrix contributions are not in canonical order."};
|
||||||
|
}
|
||||||
|
|
||||||
|
using Coordinate = std::pair<std::size_t, std::size_t>;
|
||||||
|
std::vector<Coordinate> coordinates;
|
||||||
|
coordinates.reserve(order + canonical.size());
|
||||||
|
for (std::size_t row = 0; row < order; ++row) {
|
||||||
|
coordinates.emplace_back(row, row);
|
||||||
|
}
|
||||||
|
for (const MatrixContribution& contribution : canonical) {
|
||||||
|
if (contribution.row > contribution.column ||
|
||||||
|
contribution.column >= order) {
|
||||||
|
throw std::invalid_argument{
|
||||||
|
"Matrix contribution is outside the upper triangle."};
|
||||||
|
}
|
||||||
|
coordinates.emplace_back(
|
||||||
|
contribution.row, contribution.column);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::sort(coordinates);
|
||||||
|
coordinates.erase(
|
||||||
|
std::ranges::unique(coordinates).begin(),
|
||||||
|
coordinates.end());
|
||||||
|
|
||||||
|
SymmetricCsr matrix{
|
||||||
|
order,
|
||||||
|
std::vector<std::int32_t>(order + 1, 0),
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
matrix.column_indices.reserve(coordinates.size());
|
||||||
|
for (const auto [row, column] : coordinates) {
|
||||||
|
++matrix.row_offsets[row + 1];
|
||||||
|
matrix.column_indices.push_back(csr_index(column));
|
||||||
|
}
|
||||||
|
for (std::size_t row = 0; row < order; ++row) {
|
||||||
|
const std::size_t offset =
|
||||||
|
static_cast<std::size_t>(matrix.row_offsets[row]) +
|
||||||
|
static_cast<std::size_t>(matrix.row_offsets[row + 1]);
|
||||||
|
matrix.row_offsets[row + 1] = csr_index(offset);
|
||||||
|
}
|
||||||
|
matrix.values.resize(matrix.column_indices.size(), 0.0);
|
||||||
|
|
||||||
|
std::size_t contribution_index = 0;
|
||||||
|
while (contribution_index < canonical.size()) {
|
||||||
|
const MatrixContribution& first =
|
||||||
|
canonical[contribution_index];
|
||||||
|
double value = 0.0;
|
||||||
|
do {
|
||||||
|
value += canonical[contribution_index].value;
|
||||||
|
++contribution_index;
|
||||||
|
} while (
|
||||||
|
contribution_index < canonical.size() &&
|
||||||
|
canonical[contribution_index].row == first.row &&
|
||||||
|
canonical[contribution_index].column == first.column);
|
||||||
|
|
||||||
|
const auto row_begin =
|
||||||
|
matrix.column_indices.begin() + matrix.row_offsets[first.row];
|
||||||
|
const auto row_end =
|
||||||
|
matrix.column_indices.begin() +
|
||||||
|
matrix.row_offsets[first.row + 1];
|
||||||
|
const auto entry = std::lower_bound(
|
||||||
|
row_begin,
|
||||||
|
row_end,
|
||||||
|
csr_index(first.column));
|
||||||
|
if (entry == row_end || *entry != csr_index(first.column)) {
|
||||||
|
throw std::logic_error{
|
||||||
|
"Numeric contribution is absent from the CSR pattern."};
|
||||||
|
}
|
||||||
|
matrix.values[static_cast<std::size_t>(
|
||||||
|
std::distance(matrix.column_indices.begin(), entry))] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -397,6 +397,18 @@ add_test(
|
|||||||
--gtest_filter=SymmetricCsr.*
|
--gtest_filter=SymmetricCsr.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CanonicalContribution
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_serial_assembly_tests>"
|
||||||
|
--gtest_filter=CanonicalContribution.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME DeterministicMerge
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_serial_assembly_tests>"
|
||||||
|
--gtest_filter=DeterministicMerge.*
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_constraint_tests
|
add_executable(fesa_constraint_tests
|
||||||
unit/constraints/essential_bc_test.cpp
|
unit/constraints/essential_bc_test.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <fesa/assembly/contribution.hpp>
|
||||||
#include <fesa/assembly/serial_assembler.hpp>
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -187,8 +188,9 @@ fesa::Domain build_rounding_domain(const bool large_element_first) {
|
|||||||
storage_order = {2, 0, 1};
|
storage_order = {2, 0, 1};
|
||||||
}
|
}
|
||||||
for (const std::size_t index : storage_order) {
|
for (const std::size_t index : storage_order) {
|
||||||
|
constexpr std::array<std::int64_t, 3> element_ids{1, 2, 0};
|
||||||
builder.add_beam_element({
|
builder.add_beam_element({
|
||||||
fesa::ElementId{static_cast<std::int64_t>(index)},
|
fesa::ElementId{element_ids[index]},
|
||||||
fesa::EntityOrigin{
|
fesa::EntityOrigin{
|
||||||
"BeamPart",
|
"BeamPart",
|
||||||
"Beam-1",
|
"Beam-1",
|
||||||
@@ -250,6 +252,80 @@ double csr_value(
|
|||||||
std::distance(matrix.column_indices.begin(), found))];
|
std::distance(matrix.column_indices.begin(), found))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint64_t> value_bits(
|
||||||
|
const fesa::SymmetricCsr& matrix) {
|
||||||
|
std::vector<std::uint64_t> bits;
|
||||||
|
bits.reserve(matrix.values.size());
|
||||||
|
for (const double value : matrix.values) {
|
||||||
|
bits.push_back(std::bit_cast<std::uint64_t>(value));
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CanonicalContribution, OrdersByCoordinateElementAndLocalOrder) {
|
||||||
|
const std::vector<fesa::MatrixContribution> 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<fesa::MatrixContribution> 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<fesa::MatrixContribution> 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<fesa::MatrixContribution> canonical =
|
||||||
|
fesa::canonicalize_contributions(contributions);
|
||||||
|
const fesa::SymmetricCsr expected =
|
||||||
|
fesa::merge_contributions(2, canonical);
|
||||||
|
|
||||||
|
std::array<std::size_t, 5> permutation{0, 1, 2, 3, 4};
|
||||||
|
do {
|
||||||
|
std::vector<fesa::MatrixContribution> shuffled;
|
||||||
|
shuffled.reserve(contributions.size());
|
||||||
|
for (const std::size_t index : permutation) {
|
||||||
|
shuffled.push_back(contributions[index]);
|
||||||
|
}
|
||||||
|
const std::vector<fesa::MatrixContribution> 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<std::uint64_t>(csr_value(expected, 0, 1)),
|
||||||
|
std::bit_cast<std::uint64_t>(1.0));
|
||||||
|
EXPECT_EQ(
|
||||||
|
std::bit_cast<std::uint64_t>(csr_value(expected, 1, 1)),
|
||||||
|
std::bit_cast<std::uint64_t>(+0.0));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(SparsePattern, BuildsExpectedTwoElementChainStructure) {
|
TEST(SparsePattern, BuildsExpectedTwoElementChainStructure) {
|
||||||
const fesa::Domain domain = build_chain_domain(false);
|
const fesa::Domain domain = build_chain_domain(false);
|
||||||
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
||||||
|
|||||||
Reference in New Issue
Block a user