feat(cpp-object-oriented-modular-refactoring): step 5 - solver-workflow-google-style
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user