feat(equation-and-linear-solve): step 0 — symmetric-csr-assembly
This commit is contained in:
@@ -2,5 +2,6 @@ __pycache__/
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
.harness/build/
|
.harness/build/
|
||||||
|
*pytest-basetemp*/
|
||||||
out/
|
out/
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ include(CTest)
|
|||||||
include(cmake/FesaDependencies.cmake)
|
include(cmake/FesaDependencies.cmake)
|
||||||
|
|
||||||
add_library(fesa_core STATIC
|
add_library(fesa_core STATIC
|
||||||
|
src/fesa/assembly/serial_assembler.cpp
|
||||||
src/fesa/core/version.cpp
|
src/fesa/core/version.cpp
|
||||||
src/fesa/elements/beam/beam3d2.cpp
|
src/fesa/elements/beam/beam3d2.cpp
|
||||||
src/fesa/fem/beam_frame.cpp
|
src/fesa/fem/beam_frame.cpp
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/assembly/symmetric_csr.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct EquationSystem final {
|
||||||
|
SymmetricCsr stiffness;
|
||||||
|
std::vector<double> force;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/assembly/equation_system.hpp>
|
||||||
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
[[nodiscard]] EquationSystem assemble_serial(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
// Zero-based CSR containing only the upper triangle. Column indices are
|
||||||
|
// strictly increasing within each row.
|
||||||
|
struct SymmetricCsr final {
|
||||||
|
std::size_t order;
|
||||||
|
std::vector<std::int32_t> row_offsets;
|
||||||
|
std::vector<std::int32_t> column_indices;
|
||||||
|
std::vector<double> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -34,13 +34,13 @@ public:
|
|||||||
[[nodiscard]] std::size_t free_equation_count() const noexcept;
|
[[nodiscard]] std::size_t free_equation_count() const noexcept;
|
||||||
[[nodiscard]] std::optional<std::size_t> equation(
|
[[nodiscard]] std::optional<std::size_t> equation(
|
||||||
DofAddress address) const;
|
DofAddress address) const;
|
||||||
|
[[nodiscard]] std::size_t full_dof(DofAddress address) const;
|
||||||
[[nodiscard]] std::array<std::size_t, 12> element_full_dofs(
|
[[nodiscard]] std::array<std::size_t, 12> element_full_dofs(
|
||||||
const BeamElement& element) const;
|
const BeamElement& element) const;
|
||||||
[[nodiscard]] std::vector<double> reconstruct_full(
|
[[nodiscard]] std::vector<double> reconstruct_full(
|
||||||
std::span<const double> reduced) const;
|
std::span<const double> reduced) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
[[nodiscard]] std::size_t full_dof(DofAddress address) const;
|
|
||||||
[[nodiscard]] std::size_t node_full_dof_base(NodeId node) const;
|
[[nodiscard]] std::size_t node_full_dof_base(NodeId node) const;
|
||||||
|
|
||||||
std::map<std::int64_t, std::size_t> node_full_dof_bases_;
|
std::map<std::int64_t, std::size_t> node_full_dof_bases_;
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] const Node& node(NodeId id) const;
|
[[nodiscard]] const Node& node(NodeId id) const;
|
||||||
[[nodiscard]] const Node& node(const EntityOrigin& origin) const;
|
[[nodiscard]] const Node& node(const EntityOrigin& origin) const;
|
||||||
|
[[nodiscard]] const IsotropicElastic& material(MaterialId id) const;
|
||||||
|
[[nodiscard]] const BeamSection& section(SectionId id) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class DomainBuilder;
|
friend class DomainBuilder;
|
||||||
|
|||||||
+1
-1
@@ -44,4 +44,4 @@
|
|||||||
"status": "pending"
|
"status": "pending"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iterator>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/elements/beam/beam3d2.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct NumericContribution final {
|
||||||
|
std::size_t row;
|
||||||
|
std::size_t column;
|
||||||
|
const EntityOrigin* element_origin;
|
||||||
|
std::size_t local_order;
|
||||||
|
double value;
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto origin_key(const EntityOrigin& origin) {
|
||||||
|
return std::tie(
|
||||||
|
origin.instance_name,
|
||||||
|
origin.local_label,
|
||||||
|
origin.part_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
SymmetricCsr build_sparsity_pattern(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs) {
|
||||||
|
using Coordinate = std::pair<std::size_t, std::size_t>;
|
||||||
|
std::vector<Coordinate> coordinates;
|
||||||
|
coordinates.reserve(domain.beam_elements().size() * 78);
|
||||||
|
|
||||||
|
for (const BeamElement& element : domain.beam_elements()) {
|
||||||
|
const std::array<std::size_t, 12> full_dofs =
|
||||||
|
dofs.element_full_dofs(element);
|
||||||
|
for (std::size_t local_row = 0; local_row < full_dofs.size();
|
||||||
|
++local_row) {
|
||||||
|
for (std::size_t local_column = local_row;
|
||||||
|
local_column < full_dofs.size();
|
||||||
|
++local_column) {
|
||||||
|
coordinates.emplace_back(
|
||||||
|
std::min(
|
||||||
|
full_dofs[local_row],
|
||||||
|
full_dofs[local_column]),
|
||||||
|
std::max(
|
||||||
|
full_dofs[local_row],
|
||||||
|
full_dofs[local_column]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::sort(coordinates);
|
||||||
|
coordinates.erase(
|
||||||
|
std::ranges::unique(coordinates).begin(),
|
||||||
|
coordinates.end());
|
||||||
|
|
||||||
|
SymmetricCsr pattern{
|
||||||
|
dofs.full_dof_count(),
|
||||||
|
std::vector<std::int32_t>(dofs.full_dof_count() + 1, 0),
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
pattern.column_indices.reserve(coordinates.size());
|
||||||
|
for (const auto [row, column] : coordinates) {
|
||||||
|
if (row >= pattern.order || column >= pattern.order) {
|
||||||
|
throw std::invalid_argument{
|
||||||
|
"DofManager element mapping exceeds the full system order."};
|
||||||
|
}
|
||||||
|
++pattern.row_offsets[row + 1];
|
||||||
|
pattern.column_indices.push_back(csr_index(column));
|
||||||
|
}
|
||||||
|
for (std::size_t row = 0; row < pattern.order; ++row) {
|
||||||
|
const std::size_t offset =
|
||||||
|
static_cast<std::size_t>(pattern.row_offsets[row]) +
|
||||||
|
static_cast<std::size_t>(pattern.row_offsets[row + 1]);
|
||||||
|
pattern.row_offsets[row + 1] = csr_index(offset);
|
||||||
|
}
|
||||||
|
pattern.values.resize(pattern.column_indices.size(), 0.0);
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::runtime_error kernel_error(
|
||||||
|
const BeamElement& element,
|
||||||
|
const BeamKernelResult& result) {
|
||||||
|
std::string message =
|
||||||
|
"Beam element " + std::to_string(element.origin.local_label) +
|
||||||
|
" kernel failed";
|
||||||
|
for (const Diagnostic& diagnostic : result.diagnostics) {
|
||||||
|
message += ": " + diagnostic.code + " - " + diagnostic.message;
|
||||||
|
}
|
||||||
|
return std::runtime_error{std::move(message)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NumericContribution> collect_numeric_contributions(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs) {
|
||||||
|
std::vector<NumericContribution> contributions;
|
||||||
|
contributions.reserve(domain.beam_elements().size() * 78);
|
||||||
|
|
||||||
|
for (const BeamElement& element : domain.beam_elements()) {
|
||||||
|
const BeamKernelResult result = compute_beam3d2({
|
||||||
|
{
|
||||||
|
domain.node(element.nodes[0]).position,
|
||||||
|
domain.node(element.nodes[1]).position,
|
||||||
|
},
|
||||||
|
domain.material(element.material),
|
||||||
|
domain.section(element.section),
|
||||||
|
});
|
||||||
|
if (!result.contribution.has_value()) {
|
||||||
|
throw kernel_error(element, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array<std::size_t, 12> full_dofs =
|
||||||
|
dofs.element_full_dofs(element);
|
||||||
|
for (std::size_t local_row = 0; local_row < full_dofs.size();
|
||||||
|
++local_row) {
|
||||||
|
for (std::size_t local_column = local_row;
|
||||||
|
local_column < full_dofs.size();
|
||||||
|
++local_column) {
|
||||||
|
contributions.push_back({
|
||||||
|
std::min(
|
||||||
|
full_dofs[local_row],
|
||||||
|
full_dofs[local_column]),
|
||||||
|
std::max(
|
||||||
|
full_dofs[local_row],
|
||||||
|
full_dofs[local_column]),
|
||||||
|
&element.origin,
|
||||||
|
local_row * full_dofs.size() + local_column,
|
||||||
|
result.contribution
|
||||||
|
->global_stiffness[local_row][local_column],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::sort(
|
||||||
|
contributions,
|
||||||
|
[](const NumericContribution& left,
|
||||||
|
const NumericContribution& right) {
|
||||||
|
return std::tuple{
|
||||||
|
left.row,
|
||||||
|
left.column,
|
||||||
|
origin_key(*left.element_origin),
|
||||||
|
left.local_order} <
|
||||||
|
std::tuple{
|
||||||
|
right.row,
|
||||||
|
right.column,
|
||||||
|
origin_key(*right.element_origin),
|
||||||
|
right.local_order};
|
||||||
|
});
|
||||||
|
return contributions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void merge_numeric_contributions(
|
||||||
|
SymmetricCsr& matrix,
|
||||||
|
const std::vector<NumericContribution>& contributions) {
|
||||||
|
std::size_t contribution_index = 0;
|
||||||
|
while (contribution_index < contributions.size()) {
|
||||||
|
const NumericContribution& first =
|
||||||
|
contributions[contribution_index];
|
||||||
|
double value = 0.0;
|
||||||
|
do {
|
||||||
|
value += contributions[contribution_index].value;
|
||||||
|
++contribution_index;
|
||||||
|
} while (
|
||||||
|
contribution_index < contributions.size() &&
|
||||||
|
contributions[contribution_index].row == first.row &&
|
||||||
|
contributions[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> assemble_force(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs) {
|
||||||
|
std::vector<double> force(dofs.full_dof_count(), 0.0);
|
||||||
|
for (const NodalLoad& load : domain.step().nodal_loads) {
|
||||||
|
for (std::size_t component = 0; component < load.values.size();
|
||||||
|
++component) {
|
||||||
|
const auto dof = static_cast<NodeDof>(component);
|
||||||
|
force[dofs.full_dof({load.node, dof})] +=
|
||||||
|
load.values[component];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return force;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
EquationSystem assemble_serial(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs) {
|
||||||
|
SymmetricCsr stiffness = build_sparsity_pattern(domain, dofs);
|
||||||
|
const std::vector<NumericContribution> contributions =
|
||||||
|
collect_numeric_contributions(domain, dofs);
|
||||||
|
merge_numeric_contributions(stiffness, contributions);
|
||||||
|
return {
|
||||||
|
std::move(stiffness),
|
||||||
|
assemble_force(domain, dofs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -55,6 +55,24 @@ const Node& Domain::node(const EntityOrigin& origin) const {
|
|||||||
return nodes_[found->second];
|
return nodes_[found->second];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IsotropicElastic& Domain::material(const MaterialId id) const {
|
||||||
|
const auto found = material_indices_.find(id.value());
|
||||||
|
if (found == material_indices_.end()) {
|
||||||
|
throw std::out_of_range{
|
||||||
|
"Material ID is not present in the Domain."};
|
||||||
|
}
|
||||||
|
return materials_[found->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeamSection& Domain::section(const SectionId id) const {
|
||||||
|
const auto found = section_indices_.find(id.value());
|
||||||
|
if (found == section_indices_.end()) {
|
||||||
|
throw std::out_of_range{
|
||||||
|
"Section ID is not present in the Domain."};
|
||||||
|
}
|
||||||
|
return sections_[found->second];
|
||||||
|
}
|
||||||
|
|
||||||
Domain::OriginKey Domain::origin_key(const EntityOrigin& origin) {
|
Domain::OriginKey Domain::origin_key(const EntityOrigin& origin) {
|
||||||
return {origin.instance_name, origin.local_label};
|
return {origin.instance_name, origin.local_label};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,3 +271,40 @@ add_test(
|
|||||||
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||||
--gtest_filter=RigidBody.*
|
--gtest_filter=RigidBody.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_executable(fesa_serial_assembly_tests
|
||||||
|
unit/assembly/serial_assembler_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(fesa_serial_assembly_tests PRIVATE cxx_std_20)
|
||||||
|
target_compile_options(
|
||||||
|
fesa_serial_assembly_tests
|
||||||
|
PRIVATE
|
||||||
|
/W4
|
||||||
|
/permissive-
|
||||||
|
/EHsc
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(fesa_serial_assembly_tests
|
||||||
|
PRIVATE
|
||||||
|
fesa_core
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SparsePattern
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_serial_assembly_tests>"
|
||||||
|
--gtest_filter=SparsePattern.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SerialAssembly
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_serial_assembly_tests>"
|
||||||
|
--gtest_filter=SerialAssembly.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SymmetricCsr
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_serial_assembly_tests>"
|
||||||
|
--gtest_filter=SymmetricCsr.*
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
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<fesa::Node, 3> 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<fesa::BeamElement, 2> 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<std::int64_t>& 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<std::int64_t>(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", {}, {}});
|
||||||
|
}
|
||||||
|
|
||||||
|
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<std::int32_t>(column));
|
||||||
|
if (found == end || *found != static_cast<std::int32_t>(column)) {
|
||||||
|
throw std::out_of_range{"CSR entry is not present."};
|
||||||
|
}
|
||||||
|
return matrix.values[static_cast<std::size_t>(
|
||||||
|
std::distance(matrix.column_indices.begin(), found))];
|
||||||
|
}
|
||||||
|
|
||||||
|
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<std::int32_t>{
|
||||||
|
0,
|
||||||
|
12,
|
||||||
|
23,
|
||||||
|
33,
|
||||||
|
42,
|
||||||
|
50,
|
||||||
|
57,
|
||||||
|
69,
|
||||||
|
80,
|
||||||
|
90,
|
||||||
|
99,
|
||||||
|
107,
|
||||||
|
114,
|
||||||
|
120,
|
||||||
|
125,
|
||||||
|
129,
|
||||||
|
132,
|
||||||
|
134,
|
||||||
|
135,
|
||||||
|
}));
|
||||||
|
|
||||||
|
std::vector<std::int32_t> 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<std::size_t>(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<std::int32_t>(row));
|
||||||
|
EXPECT_LT(*entry, static_cast<std::int32_t>(matrix.order));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<double>{
|
||||||
|
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, 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<double>::max();
|
||||||
|
builder.add_material(std::move(material));
|
||||||
|
auto section = unit_section();
|
||||||
|
section.area = std::numeric_limits<double>::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<void>(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
|
||||||
Reference in New Issue
Block a user