feat(cpp-object-oriented-modular-refactoring): step 5 - solver-workflow-google-style

This commit is contained in:
KOKO\Mimi
2026-08-16 06:20:08 +09:00
parent e1c0e357dd
commit 24f006fe4a
52 changed files with 5817 additions and 6369 deletions
+259 -318
View File
@@ -1,10 +1,4 @@
#include "fesa/assembly/sparse_assembler.hpp"
#include "fesa/analysis/analysis_model.hpp"
#include "fesa/assembly/parallel_for.hpp"
#include "fesa/elements/euler_beam_3d.h"
#include "fesa/elements/mitc4_shell.h"
#include "fesa/fem/dof_manager.hpp"
#include "fesa/assembly/sparse_assembler.h"
#include <array>
#include <limits>
@@ -14,6 +8,12 @@
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/euler_beam_3d.h"
#include "fesa/elements/mitc4_shell.h"
#include "fesa/fem/dof_manager.h"
namespace fesa {
namespace {
@@ -25,336 +25,277 @@ constexpr std::size_t kShellElementDofCount = 24U;
constexpr std::size_t kShellContributionCount =
kShellElementDofCount * kShellElementDofCount;
using BeamElementBuffer =
std::array<CooContribution, kBeamContributionCount>;
using ShellElementBuffer =
std::array<CooContribution, kShellContributionCount>;
using BeamElementBuffer = std::array<CooContribution, kBeamContributionCount>;
using ShellElementBuffer = std::array<CooContribution, kShellContributionCount>;
Result<SparseMatrix> assemblyFailure(
const std::string& code,
const SourceLocation& location,
const std::string& identity,
const std::string& message) {
return Result<SparseMatrix>::Failure(Status::Failure(
FailureCategory::kModel,
{{Severity::kError,
code,
location,
"*ELEMENT",
identity,
message}}));
Result<SparseMatrix> AssemblyFailure(const std::string& code,
const SourceLocation& location,
const std::string& identity,
const std::string& message) {
return Result<SparseMatrix>::Failure(Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, "*ELEMENT", identity, message}}));
}
} // namespace
} // namespace
Result<SparseMatrix> SparseAssembler::assembleStiffness(
const AnalysisModel& model,
const DofManager& dofs,
const ParallelFor& parallelFor) {
const Domain& domain = model.domain();
if (domain.Nodes().size() >
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode ||
dofs.fullDofCount() != domain.Nodes().size() * kDofsPerNode) {
return assemblyFailure(
"invalid-assembly-dimensions",
{domain.SourcePath(), 0U},
std::to_string(dofs.fullDofCount()),
"DofManager dimensions do not match the active model nodes.");
}
if (!model.activeElements().empty() && !domain.ShellElements().empty()) {
return assemblyFailure(
"unsupported-mixed-element-model",
{domain.SourcePath(), 0U},
"B33:FESA-MITC4",
"Sparse assembly does not support mixed beam and shell models.");
Result<SparseMatrix> SparseAssembler::AssembleStiffness(
const AnalysisModel& model, const DofManager& dofs,
const ParallelFor& parallel_for) {
const Domain& domain = model.GetDomain();
if (domain.Nodes().size() >
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode ||
dofs.FullDofCount() != domain.Nodes().size() * kDofsPerNode) {
return AssemblyFailure(
"invalid-assembly-dimensions", {domain.SourcePath(), 0U},
std::to_string(dofs.FullDofCount()),
"DofManager dimensions do not match the active model nodes.");
}
if (!model.ActiveElements().empty() && !domain.ShellElements().empty()) {
return AssemblyFailure(
"unsupported-mixed-element-model", {domain.SourcePath(), 0U},
"B33:FESA-MITC4",
"Sparse assembly does not support mixed beam and shell models.");
}
if (!domain.ShellElements().empty()) {
if (domain.ShellElements().size() >
(std::numeric_limits<std::size_t>::max)() / kShellContributionCount) {
return AssemblyFailure(
"invalid-assembly-dimensions", {domain.SourcePath(), 0U},
std::to_string(domain.ShellElements().size()),
"Shell contribution storage exceeds the addressable range.");
}
if (!domain.ShellElements().empty()) {
if (domain.ShellElements().size() >
(std::numeric_limits<std::size_t>::max)() /
kShellContributionCount) {
return assemblyFailure(
"invalid-assembly-dimensions",
{domain.SourcePath(), 0U},
std::to_string(domain.ShellElements().size()),
"Shell contribution storage exceeds the addressable range.");
}
std::vector<std::optional<std::array<double, 3>>> directorsByNode(
domain.Nodes().size());
for (const auto& frame : domain.ShellNodeInitialFrames()) {
if (frame.node_index >= directorsByNode.size() ||
directorsByNode[frame.node_index]) {
return assemblyFailure(
"invalid-assembly-element",
{domain.SourcePath(), 0U},
std::to_string(frame.node_index),
"Shell initial frames must map uniquely to model nodes.");
}
directorsByNode[frame.node_index] = frame.director;
}
struct ShellInput {
std::array<const Node*, 4> nodes;
std::array<std::array<double, 3>, 4> directors;
const ShellSection* section;
const LinearElasticMaterial* material;
std::array<std::size_t, kShellElementDofCount> scatter;
};
std::vector<ShellInput> inputs;
inputs.reserve(domain.ShellElements().size());
for (std::size_t elementOrder = 0U;
elementOrder < domain.ShellElements().size();
++elementOrder) {
const auto& element = domain.ShellElements()[elementOrder];
if (element.material_index >= domain.Materials().size() ||
element.section_index >= domain.ShellSections().size()) {
return assemblyFailure(
"invalid-assembly-element",
element.location,
element.source_id.source_label_text,
"Shell element references an entity outside the Domain.");
}
ShellInput input{};
input.section = &domain.ShellSections()[element.section_index];
input.material = &domain.Materials()[element.material_index];
try {
input.scatter = dofs.shellElementScatter(
static_cast<EntityIndex>(elementOrder));
} catch (const std::out_of_range&) {
return assemblyFailure(
"invalid-assembly-scatter",
element.location,
element.source_id.source_label_text,
"DofManager does not contain the active shell scatter.");
}
for (std::size_t nodePosition = 0U;
nodePosition < element.node_indices.size();
++nodePosition) {
const EntityIndex nodeIndex = element.node_indices[nodePosition];
if (nodeIndex >= domain.Nodes().size() ||
!directorsByNode[nodeIndex]) {
return assemblyFailure(
"invalid-assembly-element",
element.location,
element.source_id.source_label_text,
"Shell element requires a valid node and initial director.");
}
input.nodes[nodePosition] = &domain.Nodes()[nodeIndex];
input.directors[nodePosition] = *directorsByNode[nodeIndex];
for (std::size_t component = 0U;
component < kDofsPerNode;
++component) {
const std::size_t local =
nodePosition * kDofsPerNode + component;
const std::size_t expected =
static_cast<std::size_t>(nodeIndex) * kDofsPerNode +
component;
if (input.scatter[local] != expected ||
input.scatter[local] >= dofs.fullDofCount()) {
return assemblyFailure(
"invalid-assembly-scatter",
element.location,
element.source_id.source_label_text,
"Shell scatter does not match the active model topology.");
}
}
}
inputs.push_back(input);
}
std::vector<ShellElementBuffer> localBuffers(inputs.size());
std::vector<std::optional<Status>> localFailures(inputs.size());
parallelFor.execute(
inputs.size(),
[&](const std::size_t elementOrder) {
const auto& input = inputs[elementOrder];
const auto shell = Mitc4Shell::Create(
input.nodes,
input.directors,
*input.section,
*input.material);
if (!shell.HasValue()) {
localFailures[elementOrder] = shell.GetStatus();
return;
}
const auto stiffness = shell.Value().Stiffness();
if (!stiffness.HasValue()) {
localFailures[elementOrder] = stiffness.GetStatus();
return;
}
auto& buffer = localBuffers[elementOrder];
for (std::size_t localRow = 0U;
localRow < kShellElementDofCount;
++localRow) {
for (std::size_t localColumn = 0U;
localColumn < kShellElementDofCount;
++localColumn) {
const std::size_t localOrder =
localRow * kShellElementDofCount + localColumn;
buffer[localOrder] = {
input.scatter[localRow],
input.scatter[localColumn],
stiffness.Value().stabilized_global24(
localRow, localColumn),
elementOrder,
localOrder};
}
}
});
for (std::size_t elementOrder = 0U;
elementOrder < localFailures.size();
++elementOrder) {
if (localFailures[elementOrder]) {
return Result<SparseMatrix>::Failure(
*localFailures[elementOrder]);
}
}
std::vector<CooContribution> contributions;
contributions.reserve(
localBuffers.size() * kShellContributionCount);
// Flatten in source-element order after workers complete. The canonical
// COO reduction remains the sole writer of global CSR values.
for (const auto& buffer : localBuffers) {
contributions.insert(
contributions.end(), buffer.begin(), buffer.end());
}
return SparseMatrix::FromCoo(
dofs.fullDofCount(),
dofs.fullDofCount(),
std::move(contributions),
dofs.sparsePattern());
std::vector<std::optional<std::array<double, 3>>> directors_by_node(
domain.Nodes().size());
for (const auto& frame : domain.ShellNodeInitialFrames()) {
if (frame.node_index >= directors_by_node.size() ||
directors_by_node[frame.node_index]) {
return AssemblyFailure(
"invalid-assembly-element", {domain.SourcePath(), 0U},
std::to_string(frame.node_index),
"Shell initial frames must map uniquely to model nodes.");
}
directors_by_node[frame.node_index] = frame.director;
}
if (model.activeElements().size() >
(std::numeric_limits<std::size_t>::max)() /
kBeamContributionCount) {
return assemblyFailure(
"invalid-assembly-dimensions",
{domain.SourcePath(), 0U},
std::to_string(model.activeElements().size()),
"Element contribution storage exceeds the addressable range.");
}
struct ShellInput {
std::array<const Node*, 4> nodes;
std::array<std::array<double, 3>, 4> directors;
const ShellSection* section;
const LinearElasticMaterial* material;
std::array<std::size_t, kShellElementDofCount> scatter;
};
std::vector<ShellInput> inputs;
inputs.reserve(domain.ShellElements().size());
for (std::size_t element_order = 0U;
element_order < domain.ShellElements().size(); ++element_order) {
const auto& element = domain.ShellElements()[element_order];
if (element.material_index >= domain.Materials().size() ||
element.section_index >= domain.ShellSections().size()) {
return AssemblyFailure(
"invalid-assembly-element", element.location,
element.source_id.source_label_text,
"Shell element references an entity outside the Domain.");
}
std::vector<std::array<std::size_t, kBeamElementDofCount>> scatters;
scatters.reserve(model.activeElements().size());
for (const EntityIndex elementIndex : model.activeElements()) {
if (elementIndex >= domain.Elements().size()) {
return assemblyFailure(
"invalid-assembly-element",
{domain.SourcePath(), 0U},
std::to_string(elementIndex),
"Active element index is outside the Domain.");
ShellInput input{};
input.section = &domain.ShellSections()[element.section_index];
input.material = &domain.Materials()[element.material_index];
try {
input.scatter =
dofs.ShellElementScatter(static_cast<EntityIndex>(element_order));
} catch (const std::out_of_range&) {
return AssemblyFailure(
"invalid-assembly-scatter", element.location,
element.source_id.source_label_text,
"DofManager does not contain the active shell scatter.");
}
for (std::size_t node_position = 0U;
node_position < element.node_indices.size(); ++node_position) {
const EntityIndex node_index = element.node_indices[node_position];
if (node_index >= domain.Nodes().size() ||
!directors_by_node[node_index]) {
return AssemblyFailure(
"invalid-assembly-element", element.location,
element.source_id.source_label_text,
"Shell element requires a valid node and initial director.");
}
const auto& element = domain.Elements()[elementIndex];
if (element.node_indices[0U] >= domain.Nodes().size() ||
element.node_indices[1U] >= domain.Nodes().size() ||
element.material_index >= domain.Materials().size() ||
element.section_index >= domain.Sections().size()) {
return assemblyFailure(
"invalid-assembly-element",
element.location,
input.nodes[node_position] = &domain.Nodes()[node_index];
input.directors[node_position] = *directors_by_node[node_index];
for (std::size_t component = 0U; component < kDofsPerNode;
++component) {
const std::size_t local = node_position * kDofsPerNode + component;
const std::size_t expected =
static_cast<std::size_t>(node_index) * kDofsPerNode + component;
if (input.scatter[local] != expected ||
input.scatter[local] >= dofs.FullDofCount()) {
return AssemblyFailure(
"invalid-assembly-scatter", element.location,
element.source_id.source_label_text,
"Element references an entity outside the Domain.");
"Shell scatter does not match the active model topology.");
}
}
std::array<std::size_t, kBeamElementDofCount> scatter{};
try {
scatter = dofs.elementScatter(elementIndex);
} catch (const std::out_of_range&) {
return assemblyFailure(
"invalid-assembly-scatter",
element.location,
element.source_id.source_label_text,
"DofManager does not contain the active element scatter.");
}
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
for (std::size_t component = 0U;
component < kDofsPerNode;
++component) {
const std::size_t local = endpoint * kDofsPerNode + component;
const std::size_t expected =
static_cast<std::size_t>(element.node_indices[endpoint]) *
kDofsPerNode +
component;
if (scatter[local] != expected ||
scatter[local] >= dofs.fullDofCount()) {
return assemblyFailure(
"invalid-assembly-scatter",
element.location,
element.source_id.source_label_text,
"Element scatter does not match the active model topology.");
}
}
}
scatters.push_back(scatter);
}
inputs.push_back(input);
}
std::vector<BeamElementBuffer> localBuffers(model.activeElements().size());
std::vector<std::optional<Status>> localFailures(
model.activeElements().size());
parallelFor.execute(
model.activeElements().size(),
[&](const std::size_t elementOrder) {
const EntityIndex elementIndex = model.activeElements()[elementOrder];
const auto& definition = domain.Elements()[elementIndex];
const auto beam = EulerBeam3D::Create(
domain.Nodes()[definition.node_indices[0U]],
domain.Nodes()[definition.node_indices[1U]],
domain.Sections()[definition.section_index],
domain.Materials()[definition.material_index]);
if (!beam.HasValue()) {
localFailures[elementOrder] = beam.GetStatus();
return;
}
std::vector<ShellElementBuffer> local_buffers(inputs.size());
std::vector<std::optional<Status>> local_failures(inputs.size());
parallel_for.Execute(inputs.size(), [&](const std::size_t element_order) {
const auto& input = inputs[element_order];
const auto shell = Mitc4Shell::Create(input.nodes, input.directors,
*input.section, *input.material);
if (!shell.HasValue()) {
local_failures[element_order] = shell.GetStatus();
return;
}
const auto stiffness = shell.Value().Stiffness();
if (!stiffness.HasValue()) {
local_failures[element_order] = stiffness.GetStatus();
return;
}
const Matrix stiffness = beam.Value().GlobalStiffness();
auto& buffer = localBuffers[elementOrder];
const auto& scatter = scatters[elementOrder];
for (std::size_t localRow = 0U;
localRow < kBeamElementDofCount;
++localRow) {
for (std::size_t localColumn = 0U;
localColumn < kBeamElementDofCount;
++localColumn) {
const std::size_t localOrder =
localRow * kBeamElementDofCount + localColumn;
buffer[localOrder] = {
scatter[localRow],
scatter[localColumn],
stiffness(localRow, localColumn),
elementOrder,
localOrder};
}
}
});
for (std::size_t elementOrder = 0U;
elementOrder < localFailures.size();
++elementOrder) {
if (localFailures[elementOrder]) {
return Result<SparseMatrix>::Failure(
*localFailures[elementOrder]);
auto& buffer = local_buffers[element_order];
for (std::size_t local_row = 0U; local_row < kShellElementDofCount;
++local_row) {
for (std::size_t local_column = 0U;
local_column < kShellElementDofCount; ++local_column) {
const std::size_t local_order =
local_row * kShellElementDofCount + local_column;
buffer[local_order] = {
input.scatter[local_row], input.scatter[local_column],
stiffness.Value().stabilized_global24(local_row, local_column),
element_order, local_order};
}
}
});
for (std::size_t element_order = 0U; element_order < local_failures.size();
++element_order) {
if (local_failures[element_order]) {
return Result<SparseMatrix>::Failure(*local_failures[element_order]);
}
}
std::vector<CooContribution> contributions;
contributions.reserve(
localBuffers.size() * kBeamContributionCount);
// Flatten only after all workers complete; workers never share CSR state.
for (const auto& buffer : localBuffers) {
contributions.insert(
contributions.end(), buffer.begin(), buffer.end());
contributions.reserve(local_buffers.size() * kShellContributionCount);
// Flatten in source-element order after workers complete. The canonical
// COO reduction remains the sole writer of global CSR values.
for (const auto& buffer : local_buffers) {
contributions.insert(contributions.end(), buffer.begin(), buffer.end());
}
return SparseMatrix::FromCoo(
dofs.fullDofCount(),
dofs.fullDofCount(),
std::move(contributions),
dofs.sparsePattern());
return SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(),
std::move(contributions),
dofs.GetSparsePattern());
}
if (model.ActiveElements().size() >
(std::numeric_limits<std::size_t>::max)() / kBeamContributionCount) {
return AssemblyFailure(
"invalid-assembly-dimensions", {domain.SourcePath(), 0U},
std::to_string(model.ActiveElements().size()),
"Element contribution storage exceeds the addressable range.");
}
std::vector<std::array<std::size_t, kBeamElementDofCount>> scatters;
scatters.reserve(model.ActiveElements().size());
for (const EntityIndex element_index : model.ActiveElements()) {
if (element_index >= domain.Elements().size()) {
return AssemblyFailure("invalid-assembly-element",
{domain.SourcePath(), 0U},
std::to_string(element_index),
"Active element index is outside the Domain.");
}
const auto& element = domain.Elements()[element_index];
if (element.node_indices[0U] >= domain.Nodes().size() ||
element.node_indices[1U] >= domain.Nodes().size() ||
element.material_index >= domain.Materials().size() ||
element.section_index >= domain.Sections().size()) {
return AssemblyFailure(
"invalid-assembly-element", element.location,
element.source_id.source_label_text,
"Element references an entity outside the Domain.");
}
std::array<std::size_t, kBeamElementDofCount> scatter{};
try {
scatter = dofs.ElementScatter(element_index);
} catch (const std::out_of_range&) {
return AssemblyFailure(
"invalid-assembly-scatter", element.location,
element.source_id.source_label_text,
"DofManager does not contain the active element scatter.");
}
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
for (std::size_t component = 0U; component < kDofsPerNode; ++component) {
const std::size_t local = endpoint * kDofsPerNode + component;
const std::size_t expected =
static_cast<std::size_t>(element.node_indices[endpoint]) *
kDofsPerNode +
component;
if (scatter[local] != expected ||
scatter[local] >= dofs.FullDofCount()) {
return AssemblyFailure(
"invalid-assembly-scatter", element.location,
element.source_id.source_label_text,
"Element scatter does not match the active model topology.");
}
}
}
scatters.push_back(scatter);
}
std::vector<BeamElementBuffer> local_buffers(model.ActiveElements().size());
std::vector<std::optional<Status>> local_failures(
model.ActiveElements().size());
parallel_for.Execute(
model.ActiveElements().size(), [&](const std::size_t element_order) {
const EntityIndex element_index = model.ActiveElements()[element_order];
const auto& definition = domain.Elements()[element_index];
const auto beam =
EulerBeam3D::Create(domain.Nodes()[definition.node_indices[0U]],
domain.Nodes()[definition.node_indices[1U]],
domain.Sections()[definition.section_index],
domain.Materials()[definition.material_index]);
if (!beam.HasValue()) {
local_failures[element_order] = beam.GetStatus();
return;
}
const Matrix stiffness = beam.Value().GlobalStiffness();
auto& buffer = local_buffers[element_order];
const auto& scatter = scatters[element_order];
for (std::size_t local_row = 0U; local_row < kBeamElementDofCount;
++local_row) {
for (std::size_t local_column = 0U;
local_column < kBeamElementDofCount; ++local_column) {
const std::size_t local_order =
local_row * kBeamElementDofCount + local_column;
buffer[local_order] = {scatter[local_row], scatter[local_column],
stiffness(local_row, local_column),
element_order, local_order};
}
}
});
for (std::size_t element_order = 0U; element_order < local_failures.size();
++element_order) {
if (local_failures[element_order]) {
return Result<SparseMatrix>::Failure(*local_failures[element_order]);
}
}
std::vector<CooContribution> contributions;
contributions.reserve(local_buffers.size() * kBeamContributionCount);
// Flatten only after all workers complete; workers never share CSR state.
for (const auto& buffer : local_buffers) {
contributions.insert(contributions.end(), buffer.begin(), buffer.end());
}
return SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(),
std::move(contributions),
dofs.GetSparsePattern());
}
} // namespace fesa
} // namespace fesa