feat(deterministic-parallel-assembly): step 0 — canonical-contribution-order

This commit is contained in:
KOKO\Mimi
2026-08-01 23:06:21 +09:00
parent 6ac474f19b
commit 40a7e6c3af
6 changed files with 251 additions and 2 deletions
+1
View File
@@ -28,6 +28,7 @@ include(cmake/FesaDependencies.cmake)
add_library(fesa_core STATIC
src/fesa/analysis/linear_static_analysis.cpp
src/fesa/analysis/run_solver.cpp
src/fesa/assembly/contribution.cpp
src/fesa/assembly/serial_assembler.cpp
src/fesa/constraints/essential_bc.cpp
src/fesa/core/version.cpp
+28
View File
@@ -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
View File
@@ -47,4 +47,4 @@
"status": "pending"
}
]
}
}
+132
View File
@@ -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
+12
View File
@@ -397,6 +397,18 @@ add_test(
--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
unit/constraints/essential_bc_test.cpp
)
+77 -1
View File
@@ -1,3 +1,4 @@
#include <fesa/assembly/contribution.hpp>
#include <fesa/assembly/serial_assembler.hpp>
#include <algorithm>
@@ -187,8 +188,9 @@ fesa::Domain build_rounding_domain(const bool large_element_first) {
storage_order = {2, 0, 1};
}
for (const std::size_t index : storage_order) {
constexpr std::array<std::int64_t, 3> element_ids{1, 2, 0};
builder.add_beam_element({
fesa::ElementId{static_cast<std::int64_t>(index)},
fesa::ElementId{element_ids[index]},
fesa::EntityOrigin{
"BeamPart",
"Beam-1",
@@ -250,6 +252,80 @@ double csr_value(
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) {
const fesa::Domain domain = build_chain_domain(false);
const fesa::DofManager dofs = fesa::DofManager::build(domain);