feat(cpp-object-oriented-modular-refactoring): step 18 - load-hierarchy

This commit is contained in:
KOKO\Mimi
2026-08-16 10:56:57 +09:00
parent 08d352ae46
commit 9ad72e6d21
18 changed files with 705 additions and 137 deletions
+1
View File
@@ -20,6 +20,7 @@ add_library(
io/abaqus/domain_mapper.cpp
io/abaqus/input_reader.cpp
io/hdf5/hdf5_results_writer.cpp
loads/concentrated_nodal_load.cpp
materials/isotropic_linear_elastic_material.cpp
math/dense_blas_internal.cpp
math/matrix.cpp
+7 -7
View File
@@ -6,7 +6,7 @@
namespace fesa {
Result<AnalysisModel> AnalysisModel::Create(const Domain& domain) {
if (domain.Steps().empty()) {
if (domain.Steps().Empty()) {
return Result<AnalysisModel>::Failure(
Status::Failure(FailureCategory::kInput,
{{Severity::kError,
@@ -16,12 +16,12 @@ Result<AnalysisModel> AnalysisModel::Create(const Domain& domain) {
"0",
"AnalysisModel requires exactly one static step."}}));
}
if (domain.Steps().size() > 1U) {
if (domain.Steps().Size() > 1U) {
const auto& second_step = domain.Steps()[1];
return Result<AnalysisModel>::Failure(
Status::Failure(FailureCategory::kInput,
{{Severity::kError, "unsupported-multiple-step",
second_step.location, "STEP", second_step.name,
second_step.Location(), "STEP", second_step.Name(),
"AnalysisModel does not support multiple steps."}}));
}
return Result<AnalysisModel>::Success(AnalysisModel{domain});
@@ -29,8 +29,8 @@ Result<AnalysisModel> AnalysisModel::Create(const Domain& domain) {
const Domain& AnalysisModel::GetDomain() const noexcept { return *domain_; }
const StaticStepDefinition& AnalysisModel::Step() const noexcept {
return domain_->Steps().front();
const StepDefinition& AnalysisModel::Step() const noexcept {
return domain_->Steps()[0U];
}
const std::vector<EntityIndex>& AnalysisModel::ActiveElements() const noexcept {
@@ -101,10 +101,10 @@ AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} {
}
}
for (std::size_t index = 0U; index < Step().boundaries.size(); ++index) {
for (std::size_t index = 0U; index < Step().Boundaries().size(); ++index) {
active_boundary_conditions_.push_back(static_cast<EntityIndex>(index));
}
for (std::size_t index = 0U; index < Step().loads.size(); ++index) {
for (std::size_t index = 0U; index < Step().Loads().size(); ++index) {
active_loads_.push_back(static_cast<EntityIndex>(index));
}
}
+73 -67
View File
@@ -9,6 +9,7 @@
#include <vector>
#include "fesa/constraints/essential_constraints.h"
#include "fesa/loads/load.h"
#include "fesa/model/source_target_resolver.h"
namespace fesa {
@@ -25,33 +26,6 @@ Status LoadFailure(const std::string& code, const SourceLocation& location,
{{Severity::kError, code, location, keyword, identity, message}});
}
Result<std::vector<EntityIndex>> ResolveTarget(
const SourceTargetResolver& resolver, const Domain& domain,
const NodalLoad& load) {
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", load.target});
if (!resolved.HasValue()) {
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."));
}
std::vector<EntityIndex> nodes;
nodes.reserve(resolved.Value().size());
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
for (const auto& target : resolved.Value()) {
const EntityIndex node = target.entity_index;
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;
nodes.push_back(node);
}
return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
}
Status ValidateFiniteVector(const Vector& values,
const SourceLocation& location,
const std::string& identity) {
@@ -125,6 +99,35 @@ Status ValidateShellMoments(const Domain& domain, const Vector& full_load) {
Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
const DofManager& dofs) {
const auto& active_loads = model.ActiveLoads();
const auto& owned_loads = model.Step().Loads();
if (active_loads.size() != owned_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."));
}
LoadView loads;
loads.reserve(active_loads.size());
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 >= owned_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."));
}
loads.push_back(owned_loads[load_index]);
}
return AssembleFullNodalLoad(model, dofs, loads);
}
Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
const DofManager& dofs,
const LoadView& loads) {
const Domain& domain = model.GetDomain();
if (domain.Nodes().size() >
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode) {
@@ -165,52 +168,55 @@ Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
}
}
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."));
}
const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain);
const SourceTargetResolver target_resolver{target_index};
const LoadContext context{domain, dofs, target_resolver};
std::vector<std::vector<LoadContribution>> contributions_by_load;
contributions_by_load.reserve(loads.size());
for (std::size_t source_order = 0U; source_order < loads.size();
++source_order) {
auto contributions =
loads[source_order].get().ComputeContributions(context);
if (!contributions.HasValue()) {
return Result<Vector>::Failure(contributions.GetStatus());
}
for (const auto& contribution : contributions.Value()) {
if (contribution.source_order != source_order) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-order", model.Step().Location(), "LOAD_ASSEMBLER",
std::to_string(contribution.source_order),
"Every contribution must retain its supplying load source "
"order."));
}
if (contribution.full_dof_index >= expected_full_count) {
return Result<Vector>::Failure(LoadFailure(
"invalid-load-index", model.Step().Location(), "LOAD_ASSEMBLER",
std::to_string(contribution.full_dof_index),
"A load contribution full-DOF index is outside the active "
"model."));
}
if (!std::isfinite(contribution.value)) {
return Result<Vector>::Failure(
LoadFailure("nonfinite-load-value", model.Step().Location(),
"LOAD_ASSEMBLER", std::to_string(source_order),
"A load contribution value must be finite."));
}
}
contributions_by_load.push_back(std::move(contributions.Value()));
}
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."));
}
auto target = ResolveTarget(target_resolver, 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 full_dof = dofs.FullDof(node, component);
const double accumulated = full_load[full_dof] + load.magnitude;
// Contribution validation completes before this fixed-order candidate
// accumulation begins, so failures cannot expose a partial global vector.
for (const auto& contributions : contributions_by_load) {
for (const auto& contribution : contributions) {
const std::size_t full_dof = contribution.full_dof_index;
const double accumulated = full_load[full_dof] + contribution.value;
if (!std::isfinite(accumulated)) {
return Result<Vector>::Failure(LoadFailure(
"nonfinite-load-accumulation", load.location, "CLOAD", load.target,
"nonfinite-load-accumulation", model.Step().Location(),
"LOAD_ASSEMBLER", std::to_string(full_dof),
"Source-order load accumulation produced a nonfinite value."));
}
full_load[full_dof] = accumulated;
+2 -1
View File
@@ -180,7 +180,8 @@ Status DofManager::BuildLayouts(const AnalysisModel& analysis_model,
std::vector<std::optional<double>> prescribed_by_full_dof(full_count);
for (const EntityIndex boundary_index :
analysis_model.ActiveBoundaryConditions()) {
const auto& boundary = analysis_model.Step().boundaries.at(boundary_index);
const auto& boundary =
analysis_model.Step().Boundaries().at(boundary_index);
const auto target = ExpandBoundaryTarget(target_resolver, boundary);
for (const EntityIndex node : target) {
for (int component = boundary.first_dof; component <= boundary.last_dof;
+124
View File
@@ -0,0 +1,124 @@
#include "fesa/loads/concentrated_nodal_load.h"
#include <cmath>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/fem/dof_manager.h"
#include "fesa/model/domain.h"
namespace fesa {
namespace {
constexpr std::size_t kDofsPerNode = 6U;
Status LoadFailure(const std::string& code, const SourceLocation& location,
const std::string& identity, const std::string& message) {
return Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, "CLOAD", identity, message}});
}
} // namespace
ConcentratedNodalLoad::ConcentratedNodalLoad(
SourceTargetQuery target, std::array<double, 6> global_components,
const std::size_t source_order)
: target_{std::move(target)},
global_components_{global_components},
source_order_{source_order} {}
ConcentratedNodalLoad::ConcentratedNodalLoad(SourceTargetQuery target,
const int source_dof,
const double magnitude,
const std::size_t source_order,
SourceLocation location)
: target_{std::move(target)},
source_order_{source_order},
location_{std::move(location)},
source_dof_{source_dof} {
if (source_dof >= 1 && source_dof <= static_cast<int>(kDofsPerNode)) {
global_components_[static_cast<std::size_t>(source_dof - 1)] = magnitude;
}
}
Result<std::vector<LoadContribution>>
ConcentratedNodalLoad::ComputeContributions(const LoadContext& context) const {
const std::string& identity = target_.target_name_or_label;
if (source_dof_ != 0 &&
(source_dof_ < 1 || source_dof_ > static_cast<int>(kDofsPerNode))) {
return Result<std::vector<LoadContribution>>::Failure(LoadFailure(
"invalid-load-dof", location_, identity,
"A nodal load component must be in the range 1 through 6."));
}
for (const double component : global_components_) {
if (!std::isfinite(component)) {
return Result<std::vector<LoadContribution>>::Failure(
LoadFailure("nonfinite-load-value", location_, identity,
"A nodal load magnitude must be finite."));
}
}
if (target_.entity_kind != SourceEntityKind::kNode) {
return Result<std::vector<LoadContribution>>::Failure(
LoadFailure("invalid-load-target", location_, identity,
"A concentrated nodal load requires a node target."));
}
auto resolved = context.target_resolver.Resolve(target_);
if (!resolved.HasValue()) {
return Result<std::vector<LoadContribution>>::Failure(LoadFailure(
"invalid-load-target", location_, identity,
"The load target must resolve unambiguously to one node or one "
"expanded node set."));
}
std::vector<unsigned char> seen(context.domain.Nodes().size(), 0U);
std::vector<LoadContribution> contributions;
contributions.reserve(resolved.Value().size() * kDofsPerNode);
for (const auto& target : resolved.Value()) {
const EntityIndex node = target.entity_index;
if (node >= context.domain.Nodes().size() || seen[node] != 0U) {
return Result<std::vector<LoadContribution>>::Failure(LoadFailure(
"invalid-load-target", location_, identity,
"The expanded node set must contain unique in-range stable node "
"identities."));
}
seen[node] = 1U;
for (std::size_t component = 0U; component < kDofsPerNode; ++component) {
try {
contributions.push_back(
{source_order_,
context.dof_manager.FullDof(node,
static_cast<DofComponent>(component)),
global_components_[component]});
} catch (const std::out_of_range&) {
return Result<std::vector<LoadContribution>>::Failure(LoadFailure(
"invalid-load-target", location_, identity,
"The resolved load target must have all six full DOFs."));
}
}
}
return Result<std::vector<LoadContribution>>::Success(
std::move(contributions));
}
const SourceTargetQuery& ConcentratedNodalLoad::Target() const noexcept {
return target_;
}
const std::array<double, 6>& ConcentratedNodalLoad::GlobalComponents()
const noexcept {
return global_components_;
}
std::size_t ConcentratedNodalLoad::SourceOrder() const noexcept {
return source_order_;
}
const SourceLocation& ConcentratedNodalLoad::Location() const noexcept {
return location_;
}
} // namespace fesa
+66 -2
View File
@@ -1,11 +1,66 @@
#include "fesa/model/domain.h"
#include <cstddef>
#include <functional>
#include <memory>
#include <utility>
namespace fesa {
StepDefinition::StepDefinition(StaticStepDefinition definition)
: name_{std::move(definition.name)},
boundaries_{std::move(definition.boundaries)},
initial_increment_{definition.initial_increment},
time_period_{definition.time_period},
minimum_increment_{definition.minimum_increment},
maximum_increment_{definition.maximum_increment},
location_{std::move(definition.location)} {
loads_.reserve(definition.loads.size());
loads_view_.reserve(definition.loads.size());
for (std::size_t source_order = 0U; source_order < definition.loads.size();
++source_order) {
auto& load = definition.loads[source_order];
auto owned = std::make_unique<ConcentratedNodalLoad>(
SourceTargetQuery{SourceEntityKind::kNode, "", load.target}, load.dof,
load.magnitude, source_order, std::move(load.location));
loads_view_.push_back(std::cref(*owned));
concentrated_loads_view_.Add(*owned);
loads_.push_back(std::move(owned));
}
}
const std::string& StepDefinition::Name() const noexcept { return name_; }
const std::vector<BoundaryCondition>& StepDefinition::Boundaries()
const noexcept {
return boundaries_;
}
const LoadView& StepDefinition::Loads() const noexcept { return loads_view_; }
const DomainCollectionView<ConcentratedNodalLoad>&
StepDefinition::ConcentratedLoads() const noexcept {
return concentrated_loads_view_;
}
double StepDefinition::InitialIncrement() const noexcept {
return initial_increment_;
}
double StepDefinition::TimePeriod() const noexcept { return time_period_; }
double StepDefinition::MinimumIncrement() const noexcept {
return minimum_increment_;
}
double StepDefinition::MaximumIncrement() const noexcept {
return maximum_increment_;
}
const SourceLocation& StepDefinition::Location() const noexcept {
return location_;
}
Result<Domain> Domain::Create(ModelDefinition definition) {
return Result<Domain>::Success(Domain{std::move(definition)});
}
@@ -66,8 +121,8 @@ const std::vector<ElementSet>& Domain::ElementSets() const noexcept {
return definition_.element_sets;
}
const std::vector<StaticStepDefinition>& Domain::Steps() const noexcept {
return definition_.steps;
const DomainCollectionView<StepDefinition>& Domain::Steps() const noexcept {
return steps_view_;
}
const std::vector<Diagnostic>& Domain::Warnings() const noexcept {
@@ -131,6 +186,15 @@ Domain::Domain(ModelDefinition definition)
element_definitions_.push_back(std::move(owned));
}
definition_.shell_elements.clear();
step_definitions_.reserve(definition_.steps.size());
for (auto& step : definition_.steps) {
auto owned =
std::unique_ptr<StepDefinition>(new StepDefinition(std::move(step)));
steps_view_.Add(*owned);
step_definitions_.push_back(std::move(owned));
}
definition_.steps.clear();
}
} // namespace fesa
+29 -13
View File
@@ -15,6 +15,7 @@
#include <vector>
#include "fesa/elements/element_factory.h"
#include "fesa/loads/concentrated_nodal_load.h"
#include "fesa/math/vector3.h"
#include "fesa/model/source_target_resolver.h"
@@ -345,11 +346,12 @@ Status AppendShellRows(const SourceLocation& location,
Result<std::vector<EntityIndex>> ResolveLoadTarget(
const SourceTargetResolver& resolver, const Domain& domain,
const NodalLoad& load) {
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", load.target});
const ConcentratedNodalLoad& load) {
auto resolved = resolver.Resolve(load.Target());
if (!resolved.HasValue()) {
return RecoveryResultFailure<std::vector<EntityIndex>>(
"invalid-node-station-entity", load.location, load.target,
"invalid-node-station-entity", load.Location(),
load.Target().target_name_or_label,
"A station-eligibility load target must resolve unambiguously.");
}
std::vector<EntityIndex> nodes;
@@ -359,7 +361,8 @@ Result<std::vector<EntityIndex>> ResolveLoadTarget(
const EntityIndex node = target.entity_index;
if (node >= domain.Nodes().size() || seen[node] != 0U) {
return RecoveryResultFailure<std::vector<EntityIndex>>(
"invalid-node-station-entity", load.location, load.target,
"invalid-node-station-entity", load.Location(),
load.Target().target_name_or_label,
"A station-eligibility node set must contain unique valid nodes.");
}
seen[node] = 1U;
@@ -789,33 +792,46 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations(
}
std::vector<unsigned char> loaded_nodes(domain.Nodes().size(), 0U);
if (model.ActiveLoads().size() != model.Step().loads.size()) {
const auto& loads = model.Step().ConcentratedLoads();
if (model.ActiveLoads().size() != loads.Size()) {
return RecoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-entity", model.Step().location, model.Step().name,
"invalid-node-station-entity", model.Step().Location(),
model.Step().Name(),
"The active load view must preserve every sole-step load.");
}
const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain);
const SourceTargetResolver target_resolver{target_index};
for (std::size_t order = 0U; order < model.ActiveLoads().size(); ++order) {
const EntityIndex load_index = model.ActiveLoads()[order];
if (load_index != order || load_index >= model.Step().loads.size()) {
if (load_index != order || load_index >= loads.Size()) {
return RecoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-entity", model.Step().location,
"invalid-node-station-entity", model.Step().Location(),
std::to_string(load_index),
"Active loads must remain in stable source order.");
}
const auto& load = model.Step().loads[load_index];
if (!std::isfinite(load.magnitude)) {
const auto& load = loads[load_index];
if (load.SourceOrder() != order) {
return RecoveryResultFailure<std::vector<NodeStationResultRow>>(
"nonfinite-node-station-value", load.location, load.target,
"Station eligibility requires finite concentrated loads.");
"invalid-node-station-entity", load.Location(),
std::to_string(load.SourceOrder()),
"Station eligibility requires stable load source order.");
}
bool has_nonzero_component = false;
for (const double component : load.GlobalComponents()) {
if (!std::isfinite(component)) {
return RecoveryResultFailure<std::vector<NodeStationResultRow>>(
"nonfinite-node-station-value", load.Location(),
load.Target().target_name_or_label,
"Station eligibility requires finite concentrated loads.");
}
has_nonzero_component = has_nonzero_component || component != 0.0;
}
auto targets = ResolveLoadTarget(target_resolver, domain, load);
if (!targets.HasValue()) {
return Result<std::vector<NodeStationResultRow>>::Failure(
targets.GetStatus());
}
if (load.magnitude != 0.0) {
if (has_nonzero_component) {
for (const EntityIndex node : targets.Value()) {
loaded_nodes[node] = 1U;
}