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
+368 -445
View File
@@ -1,6 +1,4 @@
#include "fesa/assembly/load_assembler.hpp"
#include "fesa/constraints/essential_constraints.hpp"
#include "fesa/assembly/load_assembler.h"
#include <algorithm>
#include <charconv>
@@ -13,475 +11,400 @@
#include <utility>
#include <vector>
#include "fesa/constraints/essential_constraints.h"
namespace fesa {
namespace {
constexpr std::size_t dofsPerNode = 6U;
constexpr double shellMomentProjectionTolerance = 1.0e-12;
constexpr std::size_t kDofsPerNode = 6U;
constexpr double kShellMomentProjectionTolerance = 1.0e-12;
Status loadFailure(
const std::string& code,
const SourceLocation& location,
const std::string& keyword,
const std::string& identity,
const std::string& message) {
return Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, keyword, identity, message}});
Status LoadFailure(const std::string& code, const SourceLocation& location,
const std::string& keyword, const std::string& identity,
const std::string& message) {
return Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, keyword, identity, message}});
}
char asciiLower(const char value) {
if (value >= 'A' && value <= 'Z') {
return static_cast<char>(value + ('a' - 'A'));
}
return value;
char AsciiLower(const char value) {
if (value >= 'A' && value <= 'Z') {
return static_cast<char>(value + ('a' - 'A'));
}
return value;
}
bool equalName(const std::string& left, const std::string& right) {
return left.size() == right.size() &&
std::equal(
left.begin(),
left.end(),
right.begin(),
[](const char leftValue, const char rightValue) {
return asciiLower(leftValue) == asciiLower(rightValue);
});
bool EqualName(const std::string& left, const std::string& right) {
return left.size() == right.size() &&
std::equal(left.begin(), left.end(), right.begin(),
[](const char left_value, const char right_value) {
return AsciiLower(left_value) == AsciiLower(right_value);
});
}
bool tryPositiveInteger(const std::string& text, std::int64_t& value) {
const char* const first = text.data();
const char* const last = first + text.size();
const auto parsed = std::from_chars(first, last, value);
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
const char* const first = text.data();
const char* const last = first + text.size();
const auto parsed = std::from_chars(first, last, value);
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
}
bool isStrictlyIncreasing(const std::vector<std::size_t>& values) {
return std::adjacent_find(
values.begin(),
values.end(),
[](const std::size_t left, const std::size_t right) {
return left >= right;
}) == values.end();
/// @brief Checks that equation-space indices preserve stable full-DOF order.
bool IsStrictlyIncreasing(const std::vector<std::size_t>& values) {
return std::adjacent_find(
values.begin(), values.end(),
[](const std::size_t left, const std::size_t right) {
return left >= right;
}) == values.end();
}
Status validateDofOrder(
const DofManager& dofs,
const std::size_t expectedFullCount,
const SourceLocation& location) {
const std::size_t fullCount = dofs.fullDofCount();
const auto& freeDofs = dofs.freeDofs();
const auto& constrainedDofs = dofs.constrainedDofs();
if (fullCount != expectedFullCount ||
freeDofs.size() != dofs.freeDofCount() ||
constrainedDofs.size() != dofs.constrainedDofCount() ||
dofs.prescribedValues().Size() != constrainedDofs.size() ||
constrainedDofs.size() > fullCount ||
freeDofs.size() != fullCount - constrainedDofs.size()) {
return loadFailure(
"invalid-load-dimensions",
location,
"LOAD_ASSEMBLER",
std::to_string(fullCount),
"Full, free, constrained, prescribed, and model dimensions must agree.");
}
if (!isStrictlyIncreasing(freeDofs) ||
!isStrictlyIncreasing(constrainedDofs)) {
return loadFailure(
"invalid-load-order",
location,
"LOAD_ASSEMBLER",
std::to_string(fullCount),
"Free and constrained DOFs must use stable increasing full-DOF order.");
}
/// @brief Validates the full/free/constrained partition used by load assembly.
Status ValidateDofOrder(const DofManager& dofs,
const std::size_t expected_full_count,
const SourceLocation& location) {
const std::size_t full_count = dofs.FullDofCount();
const auto& free_dofs = dofs.FreeDofs();
const auto& constrained_dofs = dofs.ConstrainedDofs();
if (full_count != expected_full_count ||
free_dofs.size() != dofs.FreeDofCount() ||
constrained_dofs.size() != dofs.ConstrainedDofCount() ||
dofs.PrescribedValues().Size() != constrained_dofs.size() ||
constrained_dofs.size() > full_count ||
free_dofs.size() != full_count - constrained_dofs.size()) {
return LoadFailure("invalid-load-dimensions", location, "LOAD_ASSEMBLER",
std::to_string(full_count),
"Full, free, constrained, prescribed, and model "
"dimensions must agree.");
}
if (!IsStrictlyIncreasing(free_dofs) ||
!IsStrictlyIncreasing(constrained_dofs)) {
return LoadFailure(
"invalid-load-order", location, "LOAD_ASSEMBLER",
std::to_string(full_count),
"Free and constrained DOFs must use stable increasing full-DOF order.");
}
std::vector<unsigned char> ownership(fullCount, 0U);
try {
for (std::size_t equation = 0U;
equation < freeDofs.size();
++equation) {
const std::size_t fullDof = freeDofs[equation];
if (fullDof >= fullCount || ownership[fullDof] != 0U ||
dofs.freeEquation(fullDof) != equation) {
return loadFailure(
"invalid-load-order",
location,
"LOAD_ASSEMBLER",
std::to_string(fullDof),
"Free equation numbering must match stable full-DOF order.");
}
ownership[fullDof] = 1U;
}
for (const std::size_t fullDof : constrainedDofs) {
if (fullDof >= fullCount || ownership[fullDof] != 0U ||
dofs.freeEquation(fullDof).has_value()) {
return loadFailure(
"invalid-load-order",
location,
"LOAD_ASSEMBLER",
std::to_string(fullDof),
"Constrained DOFs must be unique and absent from free equations.");
}
ownership[fullDof] = 2U;
}
} catch (const std::out_of_range&) {
return loadFailure(
"invalid-load-dimensions",
location,
"LOAD_ASSEMBLER",
std::to_string(fullCount),
"DofManager equation storage must cover every full DOF.");
std::vector<unsigned char> ownership(full_count, 0U);
try {
for (std::size_t equation = 0U; equation < free_dofs.size(); ++equation) {
const std::size_t full_dof = free_dofs[equation];
if (full_dof >= full_count || ownership[full_dof] != 0U ||
dofs.FreeEquation(full_dof) != equation) {
return LoadFailure(
"invalid-load-order", location, "LOAD_ASSEMBLER",
std::to_string(full_dof),
"Free equation numbering must match stable full-DOF order.");
}
ownership[full_dof] = 1U;
}
if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) {
return loadFailure(
"invalid-load-order",
location,
"LOAD_ASSEMBLER",
std::to_string(fullCount),
"Free and constrained DOFs must partition the full range.");
for (const std::size_t full_dof : constrained_dofs) {
if (full_dof >= full_count || ownership[full_dof] != 0U ||
dofs.FreeEquation(full_dof).has_value()) {
return LoadFailure(
"invalid-load-order", location, "LOAD_ASSEMBLER",
std::to_string(full_dof),
"Constrained DOFs must be unique and absent from free equations.");
}
ownership[full_dof] = 2U;
}
} catch (const std::out_of_range&) {
return LoadFailure(
"invalid-load-dimensions", location, "LOAD_ASSEMBLER",
std::to_string(full_count),
"DofManager equation storage must cover every full DOF.");
}
if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) {
return LoadFailure(
"invalid-load-order", location, "LOAD_ASSEMBLER",
std::to_string(full_count),
"Free and constrained DOFs must partition the full range.");
}
return Status::Ok();
}
Result<std::vector<EntityIndex>> ResolveTarget(const Domain& domain,
const NodalLoad& load) {
std::vector<const NodeSet*> matching_sets;
for (const auto& set : domain.NodeSets()) {
if (EqualName(set.name, load.target)) {
matching_sets.push_back(&set);
}
}
std::vector<EntityIndex> matching_nodes;
std::int64_t label = 0;
if (TryPositiveInteger(load.target, label)) {
for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) {
if (domain.Nodes()[index].source_id.source_label == label) {
matching_nodes.push_back(static_cast<EntityIndex>(index));
}
}
}
if (matching_sets.size() > 1U || matching_nodes.size() > 1U ||
(!matching_sets.empty() && !matching_nodes.empty())) {
return Result<std::vector<EntityIndex>>::Failure(
LoadFailure("invalid-load-target", load.location, "CLOAD", load.target,
"The load target must resolve unambiguously to one node or "
"one expanded node set."));
}
if (!matching_sets.empty()) {
const auto& nodes = matching_sets.front()->node_indices;
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
for (const EntityIndex node : nodes) {
if (node >= domain.Nodes().size() || seen[node] != 0U) {
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
"invalid-load-target", load.location, "CLOAD", load.target,
"The expanded node set must contain unique in-range stable node "
"identities."));
}
seen[node] = 1U;
}
return Result<std::vector<EntityIndex>>::Success(nodes);
}
if (!matching_nodes.empty()) {
return Result<std::vector<EntityIndex>>::Success(std::move(matching_nodes));
}
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
"invalid-load-target", load.location, "CLOAD", load.target,
"The load target must resolve to one semantic node or node set."));
}
Status ValidateFiniteVector(const Vector& values,
const SourceLocation& location,
const std::string& identity) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return LoadFailure("nonfinite-load-value", location, "LOAD_ASSEMBLER",
identity + ":" + std::to_string(index),
"Load and prescribed displacement vectors must "
"contain finite values.");
}
}
return Status::Ok();
}
Status ValidateShellMoments(const Domain& domain, const Vector& full_load) {
if (domain.ShellElements().empty()) {
return Status::Ok();
}
std::vector<const ShellNodeInitialFrame*> frame_by_node(domain.Nodes().size(),
nullptr);
for (const auto& frame : domain.ShellNodeInitialFrames()) {
if (frame.node_index >= frame_by_node.size() ||
frame_by_node[frame.node_index] != nullptr) {
return LoadFailure(
"invalid-shell-director", {domain.SourcePath(), 0U}, "NODE",
std::to_string(frame.node_index),
"Shell nodal directors must have unique in-range node identities.");
}
frame_by_node[frame.node_index] = &frame;
}
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const double moment_x = full_load[node * kDofsPerNode + 3U];
const double moment_y = full_load[node * kDofsPerNode + 4U];
const double moment_z = full_load[node * kDofsPerNode + 5U];
if (moment_x == 0.0 && moment_y == 0.0 && moment_z == 0.0) {
continue;
}
const auto* const frame = frame_by_node[node];
if (frame == nullptr) {
return LoadFailure(
"invalid-shell-director", domain.Nodes()[node].location, "NODE",
domain.Nodes()[node].source_id.source_label_text,
"A loaded shell node must have an approved initial director.");
}
const double moment_scale = std::max(
std::abs(moment_x), std::max(std::abs(moment_y), std::abs(moment_z)));
const double scaled_x = moment_x / moment_scale;
const double scaled_y = moment_y / moment_scale;
const double scaled_z = moment_z / moment_scale;
const double scaled_norm = std::hypot(scaled_x, scaled_y, scaled_z);
const double scaled_dot = frame->director[0U] * scaled_x +
frame->director[1U] * scaled_y +
frame->director[2U] * scaled_z;
const double projection_ratio = std::abs(scaled_dot) / scaled_norm;
if (!(projection_ratio <= kShellMomentProjectionTolerance)) {
return LoadFailure("unsupported-drilling-load",
domain.Nodes()[node].location, "CLOAD",
domain.Nodes()[node].source_id.source_label_text,
"The aggregate nodal moment has an unsupported "
"director-parallel component.");
}
}
return Status::Ok();
}
Result<std::vector<EntityIndex>> resolveTarget(
const Domain& domain,
const NodalLoad& load) {
std::vector<const NodeSet*> matchingSets;
for (const auto& set : domain.NodeSets()) {
if (equalName(set.name, load.target)) {
matchingSets.push_back(&set);
} // namespace
Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
const DofManager& dofs) {
const Domain& domain = model.GetDomain();
if (domain.Nodes().size() >
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-dimensions", {domain.SourcePath(), 0U}, "LOAD_ASSEMBLER",
domain.SourceContentIdentity(),
"The semantic node count cannot be represented in full-DOF order."));
}
const std::size_t expected_full_count = domain.Nodes().size() * kDofsPerNode;
const Status dof_status =
ValidateDofOrder(dofs, expected_full_count, {domain.SourcePath(), 0U});
if (!dof_status.IsOk()) {
return Result<Vector>::Failure(dof_status);
}
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
for (std::size_t component = 0U; component < kDofsPerNode; ++component) {
try {
if (dofs.FullDof(static_cast<EntityIndex>(node),
static_cast<DofComponent>(component)) !=
node * kDofsPerNode + component) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-order", domain.Nodes()[node].location,
"LOAD_ASSEMBLER",
domain.Nodes()[node].source_id.source_label_text,
"DofManager node/component identity must match full-DOF order."));
}
} catch (const std::out_of_range&) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-dimensions", domain.Nodes()[node].location,
"LOAD_ASSEMBLER", domain.Nodes()[node].source_id.source_label_text,
"DofManager must provide all six DOFs for every semantic node."));
}
}
}
const auto& active_loads = model.ActiveLoads();
const auto& loads = model.Step().loads;
if (active_loads.size() != loads.size()) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-order", model.Step().location, "CLOAD", model.Step().name,
"The active load view must include every sole-step load once."));
}
Vector full_load{expected_full_count};
// Active load indices are required to be the original source order; this
// loop is therefore also the fixed floating-point accumulation order.
for (std::size_t source_order = 0U; source_order < active_loads.size();
++source_order) {
const EntityIndex load_index = active_loads[source_order];
if (static_cast<std::size_t>(load_index) != source_order ||
load_index >= loads.size()) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-order", model.Step().location, "CLOAD",
std::to_string(source_order),
"Active loads must retain complete stable source order."));
}
const auto& load = loads[load_index];
if (load.dof < 1 || load.dof > static_cast<int>(kDofsPerNode)) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-dof", load.location, "CLOAD", load.target,
"A nodal load component must be in the range 1 through 6."));
}
if (!std::isfinite(load.magnitude)) {
return Result<Vector>::Failure(
LoadFailure("nonfinite-load-value", load.location, "CLOAD",
load.target, "A nodal load magnitude must be finite."));
}
std::vector<EntityIndex> matchingNodes;
std::int64_t label = 0;
if (tryPositiveInteger(load.target, label)) {
for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) {
if (domain.Nodes()[index].source_id.source_label == label) {
matchingNodes.push_back(static_cast<EntityIndex>(index));
}
}
auto target = ResolveTarget(domain, load);
if (!target.HasValue()) {
return Result<Vector>::Failure(target.GetStatus());
}
if (matchingSets.size() > 1U || matchingNodes.size() > 1U ||
(!matchingSets.empty() && !matchingNodes.empty())) {
return Result<std::vector<EntityIndex>>::Failure(loadFailure(
"invalid-load-target",
load.location,
"CLOAD",
load.target,
"The load target must resolve unambiguously to one node or one expanded node set."));
const auto component = static_cast<DofComponent>(load.dof - 1);
for (const EntityIndex node : target.Value()) {
const std::size_t full_dof = dofs.FullDof(node, component);
const double accumulated = full_load[full_dof] + load.magnitude;
if (!std::isfinite(accumulated)) {
return Result<Vector>::Failure(LoadFailure(
"nonfinite-load-accumulation", load.location, "CLOAD", load.target,
"Source-order load accumulation produced a nonfinite value."));
}
full_load[full_dof] = accumulated;
}
if (!matchingSets.empty()) {
const auto& nodes = matchingSets.front()->node_indices;
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
for (const EntityIndex node : nodes) {
if (node >= domain.Nodes().size() || seen[node] != 0U) {
return Result<std::vector<EntityIndex>>::Failure(loadFailure(
"invalid-load-target",
load.location,
"CLOAD",
load.target,
"The expanded node set must contain unique in-range stable node identities."));
}
seen[node] = 1U;
}
return Result<std::vector<EntityIndex>>::Success(nodes);
}
if (!matchingNodes.empty()) {
return Result<std::vector<EntityIndex>>::Success(
std::move(matchingNodes));
}
return Result<std::vector<EntityIndex>>::Failure(loadFailure(
"invalid-load-target",
load.location,
"CLOAD",
load.target,
"The load target must resolve to one semantic node or node set."));
}
const Status shell_moment_status = ValidateShellMoments(domain, full_load);
if (!shell_moment_status.IsOk()) {
return Result<Vector>::Failure(shell_moment_status);
}
return Result<Vector>::Success(std::move(full_load));
}
Status validateFiniteVector(
const Vector& values,
const SourceLocation& location,
const std::string& identity) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return loadFailure(
"nonfinite-load-value",
location,
"LOAD_ASSEMBLER",
identity + ":" + std::to_string(index),
"Load and prescribed displacement vectors must contain finite values.");
}
Result<Vector> LoadAssembler::EffectiveFreeRhs(const Vector& full_load,
const SparseMatrix& kfc,
const Vector& prescribed_values,
const DofManager& dofs) {
const SourceLocation location{{}, 0U};
const Status dof_status = ValidateDofOrder(dofs, full_load.Size(), location);
if (!dof_status.IsOk()) {
return Result<Vector>::Failure(dof_status);
}
if (kfc.Rows() != dofs.FreeDofCount() ||
kfc.Columns() != dofs.ConstrainedDofCount() ||
prescribed_values.Size() != dofs.ConstrainedDofCount()) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-dimensions", location, "LOAD_ASSEMBLER",
std::to_string(kfc.Rows()) + "x" + std::to_string(kfc.Columns()),
"Kfc rows/columns and prescribed values must match free/constrained "
"order."));
}
const Status matrix_status = kfc.Validate();
if (!matrix_status.IsOk()) {
return Result<Vector>::Failure(matrix_status);
}
const Status load_status =
ValidateFiniteVector(full_load, location, "full-load");
if (!load_status.IsOk()) {
return Result<Vector>::Failure(load_status);
}
const Status prescribed_status =
ValidateFiniteVector(prescribed_values, location, "prescribed-values");
if (!prescribed_status.IsOk()) {
return Result<Vector>::Failure(prescribed_status);
}
Vector correction{kfc.Rows()};
for (std::size_t row = 0U; row < kfc.Rows(); ++row) {
double sum = 0.0;
for (std::size_t position = kfc.RowOffsets()[row];
position < kfc.RowOffsets()[row + 1U]; ++position) {
const double product = kfc.Values()[position] *
prescribed_values[kfc.ColumnIndices()[position]];
if (!std::isfinite(product)) {
return Result<Vector>::Failure(LoadFailure(
"nonfinite-load-accumulation", location, "LOAD_ASSEMBLER",
std::to_string(row),
"Kfc times prescribed displacement produced a nonfinite product."));
}
sum += product;
if (!std::isfinite(sum)) {
return Result<Vector>::Failure(LoadFailure(
"nonfinite-load-accumulation", location, "LOAD_ASSEMBLER",
std::to_string(row),
"Kfc times prescribed displacement produced a nonfinite row sum."));
}
}
return Status::Ok();
correction[row] = sum;
}
Vector rhs = EssentialConstraints::GatherFree(full_load, dofs);
// The constrained vector is already in DofManager order, so this is the
// approved elimination equation rhs = Ff - Kfc*dc without reordering dc.
for (std::size_t row = 0U; row < rhs.Size(); ++row) {
const double value = rhs[row] - correction[row];
if (!std::isfinite(value)) {
return Result<Vector>::Failure(
LoadFailure("nonfinite-load-accumulation", location, "LOAD_ASSEMBLER",
std::to_string(row),
"Effective RHS subtraction produced a nonfinite value."));
}
rhs[row] = value;
}
return Result<Vector>::Success(std::move(rhs));
}
Status validateShellMoments(
const Domain& domain,
const Vector& fullLoad) {
if (domain.ShellElements().empty()) {
return Status::Ok();
}
std::vector<const ShellNodeInitialFrame*> frameByNode(
domain.Nodes().size(), nullptr);
for (const auto& frame : domain.ShellNodeInitialFrames()) {
if (frame.node_index >= frameByNode.size() ||
frameByNode[frame.node_index] != nullptr) {
return loadFailure(
"invalid-shell-director",
{domain.SourcePath(), 0U},
"NODE",
std::to_string(frame.node_index),
"Shell nodal directors must have unique in-range node identities.");
}
frameByNode[frame.node_index] = &frame;
}
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const double momentX = fullLoad[node * dofsPerNode + 3U];
const double momentY = fullLoad[node * dofsPerNode + 4U];
const double momentZ = fullLoad[node * dofsPerNode + 5U];
if (momentX == 0.0 && momentY == 0.0 && momentZ == 0.0) {
continue;
}
const auto* const frame = frameByNode[node];
if (frame == nullptr) {
return loadFailure(
"invalid-shell-director",
domain.Nodes()[node].location,
"NODE",
domain.Nodes()[node].source_id.source_label_text,
"A loaded shell node must have an approved initial director.");
}
const double momentScale = std::max(
std::abs(momentX),
std::max(std::abs(momentY), std::abs(momentZ)));
const double scaledX = momentX / momentScale;
const double scaledY = momentY / momentScale;
const double scaledZ = momentZ / momentScale;
const double scaledNorm = std::hypot(scaledX, scaledY, scaledZ);
const double scaledDot =
frame->director[0U] * scaledX +
frame->director[1U] * scaledY +
frame->director[2U] * scaledZ;
const double projectionRatio = std::abs(scaledDot) / scaledNorm;
if (!(projectionRatio <= shellMomentProjectionTolerance)) {
return loadFailure(
"unsupported-drilling-load",
domain.Nodes()[node].location,
"CLOAD",
domain.Nodes()[node].source_id.source_label_text,
"The aggregate nodal moment has an unsupported director-parallel component.");
}
}
return Status::Ok();
}
} // namespace
Result<Vector> LoadAssembler::assembleFullNodalLoad(
const AnalysisModel& model,
const DofManager& dofs) {
const Domain& domain = model.domain();
if (domain.Nodes().size() >
(std::numeric_limits<std::size_t>::max)() / dofsPerNode) {
return Result<Vector>::Failure(loadFailure(
"invalid-load-dimensions",
{domain.SourcePath(), 0U},
"LOAD_ASSEMBLER",
domain.SourceContentIdentity(),
"The semantic node count cannot be represented in full-DOF order."));
}
const std::size_t expectedFullCount =
domain.Nodes().size() * dofsPerNode;
const Status dofStatus = validateDofOrder(
dofs, expectedFullCount, {domain.SourcePath(), 0U});
if (!dofStatus.IsOk()) {
return Result<Vector>::Failure(dofStatus);
}
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
for (std::size_t component = 0U;
component < dofsPerNode;
++component) {
try {
if (dofs.fullDof(
static_cast<EntityIndex>(node),
static_cast<DofComponent>(component)) !=
node * dofsPerNode + component) {
return Result<Vector>::Failure(loadFailure(
"invalid-load-order",
domain.Nodes()[node].location,
"LOAD_ASSEMBLER",
domain.Nodes()[node].source_id.source_label_text,
"DofManager node/component identity must match full-DOF order."));
}
} catch (const std::out_of_range&) {
return Result<Vector>::Failure(loadFailure(
"invalid-load-dimensions",
domain.Nodes()[node].location,
"LOAD_ASSEMBLER",
domain.Nodes()[node].source_id.source_label_text,
"DofManager must provide all six DOFs for every semantic node."));
}
}
}
const auto& activeLoads = model.activeLoads();
const auto& loads = model.step().loads;
if (activeLoads.size() != loads.size()) {
return Result<Vector>::Failure(loadFailure(
"invalid-load-order",
model.step().location,
"CLOAD",
model.step().name,
"The active load view must include every sole-step load once."));
}
Vector fullLoad{expectedFullCount};
// Active load indices are required to be the original source order; this
// loop is therefore also the fixed floating-point accumulation order.
for (std::size_t sourceOrder = 0U;
sourceOrder < activeLoads.size();
++sourceOrder) {
const EntityIndex loadIndex = activeLoads[sourceOrder];
if (static_cast<std::size_t>(loadIndex) != sourceOrder ||
loadIndex >= loads.size()) {
return Result<Vector>::Failure(loadFailure(
"invalid-load-order",
model.step().location,
"CLOAD",
std::to_string(sourceOrder),
"Active loads must retain complete stable source order."));
}
const auto& load = loads[loadIndex];
if (load.dof < 1 || load.dof > static_cast<int>(dofsPerNode)) {
return Result<Vector>::Failure(loadFailure(
"invalid-load-dof",
load.location,
"CLOAD",
load.target,
"A nodal load component must be in the range 1 through 6."));
}
if (!std::isfinite(load.magnitude)) {
return Result<Vector>::Failure(loadFailure(
"nonfinite-load-value",
load.location,
"CLOAD",
load.target,
"A nodal load magnitude must be finite."));
}
auto target = resolveTarget(domain, load);
if (!target.HasValue()) {
return Result<Vector>::Failure(target.GetStatus());
}
const auto component = static_cast<DofComponent>(load.dof - 1);
for (const EntityIndex node : target.Value()) {
const std::size_t fullDof = dofs.fullDof(node, component);
const double accumulated = fullLoad[fullDof] + load.magnitude;
if (!std::isfinite(accumulated)) {
return Result<Vector>::Failure(loadFailure(
"nonfinite-load-accumulation",
load.location,
"CLOAD",
load.target,
"Source-order load accumulation produced a nonfinite value."));
}
fullLoad[fullDof] = accumulated;
}
}
const Status shellMomentStatus = validateShellMoments(domain, fullLoad);
if (!shellMomentStatus.IsOk()) {
return Result<Vector>::Failure(shellMomentStatus);
}
return Result<Vector>::Success(std::move(fullLoad));
}
Result<Vector> LoadAssembler::effectiveFreeRhs(
const Vector& fullLoad,
const SparseMatrix& kfc,
const Vector& prescribedValues,
const DofManager& dofs) {
const SourceLocation location{{}, 0U};
const Status dofStatus =
validateDofOrder(dofs, fullLoad.Size(), location);
if (!dofStatus.IsOk()) {
return Result<Vector>::Failure(dofStatus);
}
if (kfc.Rows() != dofs.freeDofCount() ||
kfc.Columns() != dofs.constrainedDofCount() ||
prescribedValues.Size() != dofs.constrainedDofCount()) {
return Result<Vector>::Failure(loadFailure(
"invalid-load-dimensions",
location,
"LOAD_ASSEMBLER",
std::to_string(kfc.Rows()) + "x" +
std::to_string(kfc.Columns()),
"Kfc rows/columns and prescribed values must match free/constrained order."));
}
const Status matrixStatus = kfc.Validate();
if (!matrixStatus.IsOk()) {
return Result<Vector>::Failure(matrixStatus);
}
const Status loadStatus =
validateFiniteVector(fullLoad, location, "full-load");
if (!loadStatus.IsOk()) {
return Result<Vector>::Failure(loadStatus);
}
const Status prescribedStatus = validateFiniteVector(
prescribedValues, location, "prescribed-values");
if (!prescribedStatus.IsOk()) {
return Result<Vector>::Failure(prescribedStatus);
}
Vector correction{kfc.Rows()};
for (std::size_t row = 0U; row < kfc.Rows(); ++row) {
double sum = 0.0;
for (std::size_t position = kfc.RowOffsets()[row];
position < kfc.RowOffsets()[row + 1U];
++position) {
const double product = kfc.Values()[position] *
prescribedValues[kfc.ColumnIndices()[position]];
if (!std::isfinite(product)) {
return Result<Vector>::Failure(loadFailure(
"nonfinite-load-accumulation",
location,
"LOAD_ASSEMBLER",
std::to_string(row),
"Kfc times prescribed displacement produced a nonfinite product."));
}
sum += product;
if (!std::isfinite(sum)) {
return Result<Vector>::Failure(loadFailure(
"nonfinite-load-accumulation",
location,
"LOAD_ASSEMBLER",
std::to_string(row),
"Kfc times prescribed displacement produced a nonfinite row sum."));
}
}
correction[row] = sum;
}
Vector rhs = EssentialConstraints::gatherFree(fullLoad, dofs);
// The constrained vector is already in DofManager order, so this is the
// approved elimination equation rhs = Ff - Kfc*dc without reordering dc.
for (std::size_t row = 0U; row < rhs.Size(); ++row) {
const double value = rhs[row] - correction[row];
if (!std::isfinite(value)) {
return Result<Vector>::Failure(loadFailure(
"nonfinite-load-accumulation",
location,
"LOAD_ASSEMBLER",
std::to_string(row),
"Effective RHS subtraction produced a nonfinite value."));
}
rhs[row] = value;
}
return Result<Vector>::Success(std::move(rhs));
}
} // namespace fesa
} // namespace fesa
+18 -20
View File
@@ -1,30 +1,28 @@
#include "fesa/assembly/parallel_for.hpp"
#include "fesa/assembly/parallel_for.h"
#include <oneapi/tbb/parallel_for.h>
namespace fesa {
void SerialParallelFor::execute(
std::size_t count,
const std::function<void(std::size_t)>& body) const {
for (std::size_t index = 0; index < count; ++index) {
body(index);
}
void SerialParallelFor::Execute(
std::size_t count, const std::function<void(std::size_t)>& body) const {
for (std::size_t index = 0; index < count; ++index) {
body(index);
}
}
void TbbParallelFor::execute(
std::size_t count,
const std::function<void(std::size_t)>& body) const {
if (count == 0U) {
return;
}
void TbbParallelFor::Execute(
std::size_t count, const std::function<void(std::size_t)>& body) const {
if (count == 0U) {
return;
}
// Use oneTBB's caller-scoped scheduler policy. This adapter does not set
// process-wide concurrency or override the later MKL/TBB oversubscription
// policy. A body exception cancels sibling tasks and is rethrown; work
// already running during cancellation may still finish its indexed slot.
oneapi::tbb::parallel_for(
std::size_t{0}, count, [&body](std::size_t index) { body(index); });
// Use oneTBB's caller-scoped scheduler policy. This adapter does not set
// process-wide concurrency or override the later MKL/TBB oversubscription
// policy. A body exception cancels sibling tasks and is rethrown; work
// already running during cancellation may still finish its indexed slot.
oneapi::tbb::parallel_for(std::size_t{0}, count,
[&body](std::size_t index) { body(index); });
}
} // namespace fesa
} // namespace fesa
+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