feat(cpp-object-oriented-modular-refactoring): step 22 - result-recovery-modules

This commit is contained in:
KOKO\Mimi
2026-08-16 12:52:11 +09:00
parent 2b0f556a79
commit f60d4edc26
14 changed files with 1373 additions and 369 deletions
+5
View File
@@ -37,7 +37,12 @@ add_library(
model/source_target_resolver.cpp
properties/general_beam_section.cpp
properties/shell_section.cpp
results/analysis_state_commit.cpp
results/beam_result_recovery.cpp
results/global_equilibrium_recovery.cpp
results/recovery_candidate.cpp
results/result_recovery.cpp
results/shell_result_recovery.cpp
solvers/linear/mkl_pardiso_solver.cpp
)
@@ -0,0 +1,86 @@
#include "results/analysis_state_commit.h"
#include <array>
#include <utility>
namespace fesa::results_internal {
Status CommitRecoveryCandidate(RecoveryCandidate candidate,
AnalysisState& state) {
const std::size_t full_count = state.Displacement().Size();
if (candidate.displacement.Size() != full_count ||
candidate.external_force.Size() != full_count ||
candidate.internal_force.Size() != full_count ||
candidate.residual.Size() != full_count ||
candidate.reaction.Size() != full_count) {
return RecoveryFailure(
"invalid-recovery-dimensions", {}, state.Identity().step_name,
"Recovery candidate vectors must match the AnalysisState dimension.");
}
if (!IsFiniteVector(candidate.displacement) ||
!IsFiniteVector(candidate.external_force) ||
!IsFiniteVector(candidate.internal_force) ||
!IsFiniteVector(candidate.residual) ||
!IsFiniteVector(candidate.reaction)) {
return RecoveryFailure(
"nonfinite-recovery-value", {}, state.Identity().step_name,
"Recovery candidate vectors must contain only finite values.");
}
const bool has_any_beam_rows = !candidate.endpoint_rows.empty() ||
!candidate.gauss_rows.empty() ||
!candidate.stress_rows.empty();
const bool has_complete_beam_rows = !candidate.endpoint_rows.empty() &&
!candidate.gauss_rows.empty() &&
!candidate.stress_rows.empty();
if (candidate.has_beam_results != has_complete_beam_rows ||
(has_any_beam_rows && !has_complete_beam_rows)) {
return RecoveryFailure(
"invalid-recovery-result-family", {}, state.Identity().step_name,
"Beam recovery flags require endpoint, Gauss, and stress rows as one "
"complete result family.");
}
const std::array<double, 6> zero_equilibrium{};
const std::array<double, 3> zero_metrics{};
const bool has_any_shell_results =
!candidate.expected_shell_elements.empty() ||
!candidate.shell.rows.empty() ||
candidate.shell.physical_strain_energy != 0.0 ||
candidate.shell.equilibrium != zero_equilibrium ||
candidate.shell.verification_metrics != zero_metrics;
const bool has_complete_shell_inventory =
!candidate.expected_shell_elements.empty() &&
!candidate.shell.rows.empty();
if (candidate.has_shell_results != has_complete_shell_inventory ||
(has_any_shell_results && !has_complete_shell_inventory)) {
return RecoveryFailure(
"invalid-recovery-result-family", {}, state.Identity().step_name,
"Shell recovery flags require expected elements and physical rows as "
"one complete result family.");
}
if (candidate.has_beam_results && candidate.has_shell_results) {
return RecoveryFailure(
"unsupported-mixed-element-model", {}, "mixed-runtime-results",
"Result recovery does not support mixed beam and shell models.");
}
AnalysisState candidate_state = state;
candidate_state.Displacement() = std::move(candidate.displacement);
candidate_state.ExternalForce() = std::move(candidate.external_force);
candidate_state.InternalForce() = std::move(candidate.internal_force);
candidate_state.Residual() = std::move(candidate.residual);
candidate_state.Reaction() = std::move(candidate.reaction);
candidate_state.EndpointResults() = std::move(candidate.endpoint_rows);
candidate_state.GaussResults() = std::move(candidate.gauss_rows);
candidate_state.StressResults() = std::move(candidate.stress_rows);
const Status shell_commit_status = candidate_state.CommitShellResults(
candidate.expected_shell_elements, std::move(candidate.shell));
if (!shell_commit_status.IsOk()) {
return shell_commit_status;
}
state = std::move(candidate_state);
return Status::Ok();
}
} // namespace fesa::results_internal
+16
View File
@@ -0,0 +1,16 @@
#ifndef FESA_RESULTS_ANALYSIS_STATE_COMMIT_H_
#define FESA_RESULTS_ANALYSIS_STATE_COMMIT_H_
#include "fesa/analysis/analysis_state.h"
#include "fesa/core/status.h"
#include "results/recovery_candidate.h"
namespace fesa::results_internal {
/// @brief Validates a complete recovery candidate and atomically updates state.
Status CommitRecoveryCandidate(RecoveryCandidate candidate,
AnalysisState& state);
} // namespace fesa::results_internal
#endif // FESA_RESULTS_ANALYSIS_STATE_COMMIT_H_
+106
View File
@@ -0,0 +1,106 @@
#include "results/beam_result_recovery.h"
#include <array>
#include <cmath>
#include <cstddef>
#include <iterator>
#include <tuple>
namespace fesa::results_internal {
Status BeamResultRecovery(const SourceLocation& location,
const SourceEntityId& source_id,
const EntityIndex expected_element,
const std::array<SourceEntityId, 2>& expected_nodes,
const BeamElementResultRows& rows,
RecoveryCandidate& candidate) {
if (rows.endpoint_rows.size() != expected_nodes.size()) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam endpoint rows must preserve endpoint 0 and 1 source order.");
}
for (std::size_t endpoint = 0U; endpoint < expected_nodes.size();
++endpoint) {
const auto& row = rows.endpoint_rows[endpoint];
if (row.element != expected_element ||
row.endpoint != static_cast<int>(endpoint) ||
!SameSourceIdentity(row.node, expected_nodes[endpoint])) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam endpoint rows must preserve active element, endpoint, and "
"source-node order.");
}
if (!IsFiniteArray(row.end_action) ||
!IsFiniteArray(row.section_resultant)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Beam endpoint actions and section resultants must remain finite.");
}
}
constexpr std::array<int, 2> expected_gauss_points{1, 2};
if (rows.gauss_rows.size() != expected_gauss_points.size()) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam Gauss rows must preserve GP1 and GP2 source order.");
}
for (std::size_t point = 0U; point < expected_gauss_points.size(); ++point) {
const auto& row = rows.gauss_rows[point];
if (row.element != expected_element ||
row.gauss_point != expected_gauss_points[point]) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam Gauss rows must preserve active element and GP1 through GP2 "
"order.");
}
if (!IsFiniteArray(row.generalized_strain) ||
!IsFiniteArray(row.generalized_resultant)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Beam generalized recovery values must remain finite.");
}
}
if (rows.stress_rows.empty() || rows.stress_rows.front().gauss_point != 1 ||
rows.stress_rows.back().gauss_point != 2) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam stress rows must preserve GP and section-point source order.");
}
for (std::size_t order = 0U; order < rows.stress_rows.size(); ++order) {
const auto& row = rows.stress_rows[order];
if (row.element != expected_element || row.gauss_point < 1 ||
row.gauss_point > 2 ||
(order != 0U && std::tie(rows.stress_rows[order - 1U].gauss_point,
rows.stress_rows[order - 1U].section_point) >=
std::tie(row.gauss_point, row.section_point))) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam stress rows must preserve active element, GP, and "
"section-point order.");
}
if (!std::isfinite(row.x1) || !std::isfinite(row.x2) ||
!std::isfinite(row.s11)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Beam section-point stress values must remain finite.");
}
}
candidate.has_beam_results = true;
candidate.endpoint_rows.insert(candidate.endpoint_rows.end(),
rows.endpoint_rows.begin(),
rows.endpoint_rows.end());
candidate.gauss_rows.insert(candidate.gauss_rows.end(),
rows.gauss_rows.begin(), rows.gauss_rows.end());
candidate.stress_rows.insert(candidate.stress_rows.end(),
rows.stress_rows.begin(),
rows.stress_rows.end());
return Status::Ok();
}
} // namespace fesa::results_internal
+23
View File
@@ -0,0 +1,23 @@
#ifndef FESA_RESULTS_BEAM_RESULT_RECOVERY_H_
#define FESA_RESULTS_BEAM_RESULT_RECOVERY_H_
#include <array>
#include "fesa/core/source_identity.h"
#include "fesa/core/status.h"
#include "fesa/elements/element.h"
#include "results/recovery_candidate.h"
namespace fesa::results_internal {
/// @brief Validates and appends distinct beam recovery row collections.
Status BeamResultRecovery(const SourceLocation& location,
const SourceEntityId& source_id,
EntityIndex expected_element,
const std::array<SourceEntityId, 2>& expected_nodes,
const BeamElementResultRows& rows,
RecoveryCandidate& candidate);
} // namespace fesa::results_internal
#endif // FESA_RESULTS_BEAM_RESULT_RECOVERY_H_
@@ -0,0 +1,269 @@
#include "results/global_equilibrium_recovery.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <string>
#include <vector>
#include "fesa/math/vector3.h"
namespace fesa::results_internal {
namespace {
constexpr std::size_t kDofsPerNode = 6U;
constexpr double kFreeResidualTolerance = 1.0e-10;
constexpr double kGlobalEquilibriumTolerance = 1.0e-10;
double IndexedNorm(const Vector& values,
const std::vector<std::size_t>& indices) {
double result = 0.0;
for (const std::size_t index : indices) {
result = std::hypot(result, values[index]);
}
return result;
}
/// @brief Computes separate Kff*df and Kfc*dc norms in stable CSR order.
std::array<double, 2> FreeEquationInternalTermNorms(
const SparseMatrix& stiffness, const Vector& displacement,
const DofManager& dofs) {
std::vector<unsigned char> free_column(dofs.FullDofCount(), 0U);
for (const std::size_t full_dof : dofs.FreeDofs()) {
free_column[full_dof] = 1U;
}
double free_term_norm = 0.0;
double constrained_term_norm = 0.0;
for (const std::size_t row : dofs.FreeDofs()) {
double free_term = 0.0;
double constrained_term = 0.0;
for (std::size_t position = stiffness.RowOffsets()[row];
position < stiffness.RowOffsets()[row + 1U]; ++position) {
const std::size_t column = stiffness.ColumnIndices()[position];
const double contribution =
stiffness.Values()[position] * displacement[column];
if (free_column[column] != 0U) {
free_term += contribution;
} else {
constrained_term += contribution;
}
}
free_term_norm = std::hypot(free_term_norm, free_term);
constrained_term_norm = std::hypot(constrained_term_norm, constrained_term);
}
return {free_term_norm, constrained_term_norm};
}
bool AccumulateVectorAndScale(std::array<double, 3>& total, double& scale,
const std::array<double, 3>& contribution) {
const Vector3 contribution_vector{contribution};
const double magnitude = contribution_vector.Norm();
const double accumulated_scale = scale + magnitude;
if (!contribution_vector.IsFinite() || !std::isfinite(magnitude) ||
!std::isfinite(accumulated_scale)) {
return false;
}
for (std::size_t component = 0U; component < contribution.size();
++component) {
const double accumulated = total[component] + contribution[component];
if (!std::isfinite(accumulated)) {
return false;
}
total[component] = accumulated;
}
scale = accumulated_scale;
return true;
}
double NormalizedBalance(const std::array<double, 3>& balance,
const double scale) {
const double balance_norm = Vector3{balance}.Norm();
if (!std::isfinite(balance_norm) || !std::isfinite(scale)) {
return (std::numeric_limits<double>::infinity)();
}
if (scale == 0.0) {
return balance_norm == 0.0 ? 0.0
: (std::numeric_limits<double>::infinity)();
}
return balance_norm / scale;
}
} // namespace
Result<GlobalEquilibriumResult> GlobalEquilibriumRecovery(
const Domain& domain, const DofManager& dofs,
const SparseMatrix& full_stiffness, const Vector& full_displacement,
const Vector& full_external_force) {
const std::size_t full_count = dofs.FullDofCount();
if (full_stiffness.Rows() != full_count ||
full_stiffness.Columns() != full_count ||
full_displacement.Size() != full_count ||
full_external_force.Size() != full_count ||
domain.Nodes().size() * kDofsPerNode != full_count) {
return RecoveryResultFailure<GlobalEquilibriumResult>(
"invalid-recovery-dimensions", {domain.SourcePath(), 0U},
domain.SourceContentIdentity(),
"Model, DOF, stiffness, and recovery vector dimensions must agree.");
}
const Status matrix_status = full_stiffness.Validate();
if (!matrix_status.IsOk()) {
return Result<GlobalEquilibriumResult>::Failure(matrix_status);
}
if (!IsFiniteVector(full_displacement) ||
!IsFiniteVector(full_external_force)) {
return RecoveryResultFailure<GlobalEquilibriumResult>(
"nonfinite-recovery-value", {domain.SourcePath(), 0U},
domain.SourceContentIdentity(),
"Displacement and external-force inputs must be finite.");
}
GlobalEquilibriumResult result{full_stiffness.Multiply(full_displacement),
Vector{full_count}, Vector{full_count}, 0.0};
if (!IsFiniteVector(result.internal_force)) {
return RecoveryResultFailure<GlobalEquilibriumResult>(
"nonfinite-recovery-value", {domain.SourcePath(), 0U},
domain.SourceContentIdentity(),
"Full stiffness multiplication must produce finite internal force.");
}
for (std::size_t full_dof = 0U; full_dof < result.residual.Size();
++full_dof) {
result.residual[full_dof] =
result.internal_force[full_dof] - full_external_force[full_dof];
if (!std::isfinite(result.residual[full_dof])) {
return RecoveryResultFailure<GlobalEquilibriumResult>(
"nonfinite-recovery-value", {domain.SourcePath(), 0U},
std::to_string(full_dof),
"Internal-minus-external residual must remain finite.");
}
}
const double residual_norm = IndexedNorm(result.residual, dofs.FreeDofs());
const auto internal_term_norms =
FreeEquationInternalTermNorms(full_stiffness, full_displacement, dofs);
const double external_norm =
IndexedNorm(full_external_force, dofs.FreeDofs());
// Normalize against Kff*df, Kfc*dc, and Ff separately; the already-cancelled
// K*d term would hide prescribed-displacement scaling.
const double denominator =
(std::max)({internal_term_norms[0U], internal_term_norms[1U],
external_norm});
if (!std::isfinite(residual_norm) || !std::isfinite(denominator)) {
return RecoveryResultFailure<GlobalEquilibriumResult>(
"nonfinite-recovery-value", {domain.SourcePath(), 0U}, "free-residual",
"Free residual and its physical normalization scale must be finite.");
}
result.normalized_free_residual =
denominator == 0.0
? (residual_norm == 0.0 ? 0.0
: (std::numeric_limits<double>::infinity)())
: residual_norm / denominator;
if (!std::isfinite(result.normalized_free_residual) ||
result.normalized_free_residual > kFreeResidualTolerance) {
return RecoveryResultFailure<GlobalEquilibriumResult>(
"free-residual-tolerance-failure", {domain.SourcePath(), 0U},
"free-residual", "The normalized free residual exceeds 1e-10.");
}
// The state reaction vector intentionally preserves free residual evidence;
// shell global equilibrium below separately filters constrained reactions.
result.reaction = result.residual;
return Result<GlobalEquilibriumResult>::Success(std::move(result));
}
Status RecoverShellGlobalEvidence(const Domain& domain, const DofManager& dofs,
const Vector& external_force,
const GlobalEquilibriumResult& global,
RecoveryCandidate& candidate) {
std::vector<unsigned char> constrained(dofs.FullDofCount(), 0U);
for (const std::size_t full_dof : dofs.ConstrainedDofs()) {
constrained[full_dof] = 1U;
}
std::array<double, 3> applied_force{};
std::array<double, 3> reaction_force{};
std::array<double, 3> applied_moment{};
std::array<double, 3> reaction_moment{};
double applied_force_scale = 0.0;
double reaction_force_scale = 0.0;
double applied_moment_scale = 0.0;
double reaction_moment_scale = 0.0;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const std::size_t offset = node * kDofsPerNode;
std::array<double, 3> nodal_applied_force{};
std::array<double, 3> nodal_reaction_force{};
std::array<double, 3> nodal_applied_moment{};
std::array<double, 3> nodal_reaction_moment{};
for (std::size_t component = 0U; component < 3U; ++component) {
nodal_applied_force[component] = external_force[offset + component];
nodal_applied_moment[component] = external_force[offset + 3U + component];
if (constrained[offset + component] != 0U) {
nodal_reaction_force[component] = global.residual[offset + component];
}
if (constrained[offset + 3U + component] != 0U) {
nodal_reaction_moment[component] =
global.residual[offset + 3U + component];
}
}
const Vector3 position{domain.Nodes()[node].coordinates};
const Vector3 applied_force_moment =
position.Cross(Vector3{nodal_applied_force});
const Vector3 reaction_force_moment =
position.Cross(Vector3{nodal_reaction_force});
for (std::size_t component = 0U; component < 3U; ++component) {
nodal_applied_moment[component] += applied_force_moment[component];
nodal_reaction_moment[component] += reaction_force_moment[component];
}
if (!AccumulateVectorAndScale(applied_force, applied_force_scale,
nodal_applied_force) ||
!AccumulateVectorAndScale(reaction_force, reaction_force_scale,
nodal_reaction_force) ||
!AccumulateVectorAndScale(applied_moment, applied_moment_scale,
nodal_applied_moment) ||
!AccumulateVectorAndScale(reaction_moment, reaction_moment_scale,
nodal_reaction_moment)) {
return RecoveryFailure("nonfinite-recovery-value",
domain.Nodes()[node].location,
domain.Nodes()[node].source_id.source_label_text,
"Global force and moment evidence must remain "
"finite in source-node order.");
}
}
const Vector3 force_balance =
Vector3{applied_force} + Vector3{reaction_force};
const Vector3 moment_balance =
Vector3{applied_moment} + Vector3{reaction_moment};
for (std::size_t component = 0U; component < 3U; ++component) {
candidate.shell.equilibrium[component] = force_balance[component];
candidate.shell.equilibrium[3U + component] = moment_balance[component];
}
const double force_metric =
NormalizedBalance(force_balance.Components(),
(std::max)(applied_force_scale, reaction_force_scale));
const double moment_metric = NormalizedBalance(
moment_balance.Components(),
(std::max)(applied_moment_scale, reaction_moment_scale));
candidate.shell.verification_metrics = {global.normalized_free_residual,
force_metric, moment_metric};
if (!IsFiniteArray(candidate.shell.equilibrium) ||
!Vector3{candidate.shell.verification_metrics}.IsFinite()) {
return RecoveryFailure("nonfinite-recovery-value",
{domain.SourcePath(), 0U}, "global-equilibrium",
"Global equilibrium values and their physical "
"normalization scales must be finite.");
}
if (force_metric > kGlobalEquilibriumTolerance ||
moment_metric > kGlobalEquilibriumTolerance) {
return RecoveryFailure(
"global-equilibrium-tolerance-failure", {domain.SourcePath(), 0U},
"global-equilibrium",
"Normalized global force or moment balance exceeds 1e-10.");
}
return Status::Ok();
}
} // namespace fesa::results_internal
@@ -0,0 +1,37 @@
#ifndef FESA_RESULTS_GLOBAL_EQUILIBRIUM_RECOVERY_H_
#define FESA_RESULTS_GLOBAL_EQUILIBRIUM_RECOVERY_H_
#include "fesa/core/status.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/sparse_matrix.h"
#include "fesa/math/vector.h"
#include "fesa/model/domain.h"
#include "results/recovery_candidate.h"
namespace fesa::results_internal {
/// @brief Owns full residual, reaction evidence, and free equilibrium scale.
struct GlobalEquilibriumResult {
Vector internal_force;
Vector residual;
Vector reaction;
double normalized_free_residual{0.0};
};
/// @brief Computes K*d-F, full reaction evidence, and free residual metric.
/// @return The complete global residual candidate or a structured failure.
Result<GlobalEquilibriumResult> GlobalEquilibriumRecovery(
const Domain& domain, const DofManager& dofs,
const SparseMatrix& full_stiffness, const Vector& full_displacement,
const Vector& full_external_force);
/// @brief Adds shell global force and moment equilibrium evidence.
/// @note Moments are taken about the fixed global origin.
Status RecoverShellGlobalEvidence(const Domain& domain, const DofManager& dofs,
const Vector& external_force,
const GlobalEquilibriumResult& global,
RecoveryCandidate& candidate);
} // namespace fesa::results_internal
#endif // FESA_RESULTS_GLOBAL_EQUILIBRIUM_RECOVERY_H_
+39
View File
@@ -0,0 +1,39 @@
#include "results/recovery_candidate.h"
#include <cstddef>
#include <string>
namespace fesa::results_internal {
RecoveryCandidate::RecoveryCandidate(const std::size_t full_dof_count)
: displacement{full_dof_count},
external_force{full_dof_count},
internal_force{full_dof_count},
residual{full_dof_count},
reaction{full_dof_count} {}
Status RecoveryFailure(const std::string& code, const SourceLocation& location,
const std::string& identity,
const std::string& message) {
return Status::Failure(FailureCategory::kModel,
{{Severity::kError, code, location, "RESULT_RECOVERY",
identity, message}});
}
bool SameSourceIdentity(const SourceEntityId& left,
const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
bool IsFiniteVector(const Vector& values) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return false;
}
}
return true;
}
} // namespace fesa::results_internal
+65
View File
@@ -0,0 +1,65 @@
#ifndef FESA_RESULTS_RECOVERY_CANDIDATE_H_
#define FESA_RESULTS_RECOVERY_CANDIDATE_H_
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <string>
#include <vector>
#include "fesa/core/source_identity.h"
#include "fesa/core/status.h"
#include "fesa/math/vector.h"
#include "fesa/results/result_records.h"
namespace fesa::results_internal {
/// @brief Owns every mutable recovery value before final state commit.
struct RecoveryCandidate {
explicit RecoveryCandidate(std::size_t full_dof_count);
Vector displacement;
Vector external_force;
Vector internal_force;
Vector residual;
Vector reaction;
std::vector<EndpointResultRow> endpoint_rows;
std::vector<GaussResultRow> gauss_rows;
std::vector<StressS11Row> stress_rows;
ShellStateCandidate shell;
std::vector<EntityIndex> expected_shell_elements;
bool has_beam_results{false};
bool has_shell_results{false};
};
/// @brief Creates a structured result-recovery model failure.
Status RecoveryFailure(const std::string& code, const SourceLocation& location,
const std::string& identity, const std::string& message);
/// @brief Creates a failed result-recovery Result with one diagnostic.
template <class T>
Result<T> RecoveryResultFailure(const std::string& code,
const SourceLocation& location,
const std::string& identity,
const std::string& message) {
return Result<T>::Failure(RecoveryFailure(code, location, identity, message));
}
/// @brief Compares preserved source identity fields exactly.
bool SameSourceIdentity(const SourceEntityId& left,
const SourceEntityId& right);
/// @brief Tests a fixed-size recovery component array for finite values.
template <std::size_t Size>
bool IsFiniteArray(const std::array<double, Size>& values) {
return std::all_of(values.begin(), values.end(),
[](const double value) { return std::isfinite(value); });
}
/// @brief Tests an owned Vector for finite values.
bool IsFiniteVector(const Vector& values);
} // namespace fesa::results_internal
#endif // FESA_RESULTS_RECOVERY_CANDIDATE_H_
+48 -369
View File
@@ -18,13 +18,16 @@
#include "fesa/loads/concentrated_nodal_load.h"
#include "fesa/math/vector3.h"
#include "fesa/model/source_target_resolver.h"
#include "results/analysis_state_commit.h"
#include "results/beam_result_recovery.h"
#include "results/global_equilibrium_recovery.h"
#include "results/recovery_candidate.h"
#include "results/shell_result_recovery.h"
namespace fesa {
namespace {
constexpr std::size_t kDofsPerNode = 6U;
constexpr double kFreeResidualTolerance = 1.0e-10;
constexpr double kGlobalEquilibriumTolerance = 1.0e-10;
constexpr double kAxisTolerance = 1.0e-12;
using AxisSet = std::array<std::array<double, 3>, 3>;
@@ -45,66 +48,9 @@ Result<T> RecoveryResultFailure(const std::string& code,
return Result<T>::Failure(RecoveryFailure(code, location, identity, message));
}
bool SameSourceIdentity(const SourceEntityId& left,
const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
template <std::size_t Size>
bool IsFinite(const std::array<double, Size>& values) {
return std::all_of(values.begin(), values.end(),
[](const double value) { return std::isfinite(value); });
}
bool IsFinite(const Vector& values) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return false;
}
}
return true;
}
double IndexedNorm(const Vector& values,
const std::vector<std::size_t>& indices) {
double result = 0.0;
for (const std::size_t index : indices) {
result = std::hypot(result, values[index]);
}
return result;
}
/// @brief Computes separate Kff*df and Kfc*dc norms in stable CSR order.
std::array<double, 2> FreeEquationInternalTermNorms(
const SparseMatrix& stiffness, const Vector& displacement,
const DofManager& dofs) {
std::vector<unsigned char> free_column(dofs.FullDofCount(), 0U);
for (const std::size_t full_dof : dofs.FreeDofs()) {
free_column[full_dof] = 1U;
}
double free_term_norm = 0.0;
double constrained_term_norm = 0.0;
for (const std::size_t row : dofs.FreeDofs()) {
double free_term = 0.0;
double constrained_term = 0.0;
for (std::size_t position = stiffness.RowOffsets()[row];
position < stiffness.RowOffsets()[row + 1U]; ++position) {
const std::size_t column = stiffness.ColumnIndices()[position];
const double contribution =
stiffness.Values()[position] * displacement[column];
if (free_column[column] != 0U) {
free_term += contribution;
} else {
constrained_term += contribution;
}
}
free_term_norm = std::hypot(free_term_norm, free_term);
constrained_term_norm = std::hypot(constrained_term_norm, constrained_term);
}
return {free_term_norm, constrained_term_norm};
return results_internal::IsFiniteArray(values);
}
/// @brief Checks that equation-space indices preserve stable full-DOF order.
@@ -157,7 +103,8 @@ Status ValidateRecoveryInputs(const AnalysisModel& model,
if (!dof_status.IsOk()) {
return dof_status;
}
if (!IsFinite(full_displacement) || !IsFinite(full_external_force)) {
if (!results_internal::IsFiniteVector(full_displacement) ||
!results_internal::IsFiniteVector(full_external_force)) {
return RecoveryFailure(
"nonfinite-recovery-value", {domain.SourcePath(), 0U},
domain.SourceContentIdentity(),
@@ -244,7 +191,8 @@ Status ValidateRecoveryInputs(const AnalysisModel& model,
previous_definition = definition_index;
const auto& definition = domain.Elements()[definition_index];
const auto& layout = elements[source_order].get().DofLayout();
if (!SameSourceIdentity(layout.source_id, definition.SourceId()) ||
if (!results_internal::SameSourceIdentity(layout.source_id,
definition.SourceId()) ||
layout.node_indices != definition.NodeIndices()) {
return RecoveryFailure(
"invalid-recovery-entity", {domain.SourcePath(), 0U},
@@ -260,90 +208,6 @@ Status ValidateRecoveryInputs(const AnalysisModel& model,
return Status::Ok();
}
/// @brief Appends one typed beam bundle without changing row identity or sign.
Status AppendBeamRows(const SourceLocation& location,
const SourceEntityId& source_id,
const EntityIndex expected_element,
const BeamElementResultRows& rows,
std::vector<EndpointResultRow>& endpoint_rows,
std::vector<GaussResultRow>& gauss_rows,
std::vector<StressS11Row>& stress_rows) {
for (const auto& row : rows.endpoint_rows) {
if (row.element != expected_element) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam endpoint rows must retain their active element identity.");
}
if (!IsFinite(row.end_action) || !IsFinite(row.section_resultant)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Beam endpoint actions and section resultants must remain finite.");
}
endpoint_rows.push_back(row);
}
for (const auto& row : rows.gauss_rows) {
if (row.element != expected_element) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam Gauss rows must retain their active element identity.");
}
if (!IsFinite(row.generalized_strain) ||
!IsFinite(row.generalized_resultant)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Beam generalized recovery values must remain finite.");
}
gauss_rows.push_back(row);
}
for (const auto& row : rows.stress_rows) {
if (row.element != expected_element) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Beam stress rows must retain their active element identity.");
}
if (!std::isfinite(row.x1) || !std::isfinite(row.x2) ||
!std::isfinite(row.s11)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Beam section-point stress values must remain finite.");
}
stress_rows.push_back(row);
}
return Status::Ok();
}
/// @brief Appends one physical shell bundle in stable runtime order.
Status AppendShellRows(const SourceLocation& location,
const SourceEntityId& source_id,
const EntityIndex expected_element,
const ShellElementResultRows& rows,
std::vector<EntityIndex>& expected_elements,
ShellStateCandidate& candidate) {
const double accumulated_energy =
candidate.physical_strain_energy + rows.physical_strain_energy;
if (!std::isfinite(rows.physical_strain_energy) ||
!std::isfinite(accumulated_energy)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Source-order physical shell energy reduction must remain finite.");
}
for (const auto& row : rows.rows) {
if (row.element != expected_element) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Shell rows must retain their active element identity.");
}
candidate.rows.push_back(row);
}
expected_elements.push_back(expected_element);
candidate.physical_strain_energy = accumulated_energy;
return Status::Ok();
}
Result<std::vector<EntityIndex>> ResolveLoadTarget(
const SourceTargetResolver& resolver, const Domain& domain,
const ConcentratedNodalLoad& load) {
@@ -371,134 +235,6 @@ Result<std::vector<EntityIndex>> ResolveLoadTarget(
return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
}
bool AccumulateVectorAndScale(std::array<double, 3>& total, double& scale,
const std::array<double, 3>& contribution) {
const Vector3 contribution_vector{contribution};
const double magnitude = contribution_vector.Norm();
const double accumulated_scale = scale + magnitude;
if (!contribution_vector.IsFinite() || !std::isfinite(magnitude) ||
!std::isfinite(accumulated_scale)) {
return false;
}
for (std::size_t component = 0U; component < contribution.size();
++component) {
const double accumulated = total[component] + contribution[component];
if (!std::isfinite(accumulated)) {
return false;
}
total[component] = accumulated;
}
scale = accumulated_scale;
return true;
}
double NormalizedBalance(const std::array<double, 3>& balance,
const double scale) {
const double balance_norm = Vector3{balance}.Norm();
if (!std::isfinite(balance_norm) || !std::isfinite(scale)) {
return (std::numeric_limits<double>::infinity)();
}
if (scale == 0.0) {
return balance_norm == 0.0 ? 0.0
: (std::numeric_limits<double>::infinity)();
}
return balance_norm / scale;
}
/// @brief Computes global shell evidence in stable source order.
Status PopulateShellGlobalEvidence(const Domain& domain, const DofManager& dofs,
const Vector& external_force,
const Vector& residual,
const double normalized_residual,
ShellStateCandidate& candidate) {
std::vector<unsigned char> constrained(dofs.FullDofCount(), 0U);
for (const std::size_t full_dof : dofs.ConstrainedDofs()) {
constrained[full_dof] = 1U;
}
std::array<double, 3> applied_force{};
std::array<double, 3> reaction_force{};
std::array<double, 3> applied_moment{};
std::array<double, 3> reaction_moment{};
double applied_force_scale = 0.0;
double reaction_force_scale = 0.0;
double applied_moment_scale = 0.0;
double reaction_moment_scale = 0.0;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const std::size_t offset = node * kDofsPerNode;
std::array<double, 3> nodal_applied_force{};
std::array<double, 3> nodal_reaction_force{};
std::array<double, 3> nodal_applied_moment{};
std::array<double, 3> nodal_reaction_moment{};
for (std::size_t component = 0U; component < 3U; ++component) {
nodal_applied_force[component] = external_force[offset + component];
nodal_applied_moment[component] = external_force[offset + 3U + component];
if (constrained[offset + component] != 0U) {
nodal_reaction_force[component] = residual[offset + component];
}
if (constrained[offset + 3U + component] != 0U) {
nodal_reaction_moment[component] = residual[offset + 3U + component];
}
}
const Vector3 position{domain.Nodes()[node].coordinates};
const Vector3 applied_force_moment =
position.Cross(Vector3{nodal_applied_force});
const Vector3 reaction_force_moment =
position.Cross(Vector3{nodal_reaction_force});
for (std::size_t component = 0U; component < 3U; ++component) {
nodal_applied_moment[component] += applied_force_moment[component];
nodal_reaction_moment[component] += reaction_force_moment[component];
}
if (!AccumulateVectorAndScale(applied_force, applied_force_scale,
nodal_applied_force) ||
!AccumulateVectorAndScale(reaction_force, reaction_force_scale,
nodal_reaction_force) ||
!AccumulateVectorAndScale(applied_moment, applied_moment_scale,
nodal_applied_moment) ||
!AccumulateVectorAndScale(reaction_moment, reaction_moment_scale,
nodal_reaction_moment)) {
return RecoveryFailure("nonfinite-recovery-value",
domain.Nodes()[node].location,
domain.Nodes()[node].source_id.source_label_text,
"Global force and moment evidence must remain "
"finite in source-node order.");
}
}
const Vector3 force_balance =
Vector3{applied_force} + Vector3{reaction_force};
const Vector3 moment_balance =
Vector3{applied_moment} + Vector3{reaction_moment};
for (std::size_t component = 0U; component < 3U; ++component) {
candidate.equilibrium[component] = force_balance[component];
candidate.equilibrium[3U + component] = moment_balance[component];
}
const double force_metric =
NormalizedBalance(force_balance.Components(),
(std::max)(applied_force_scale, reaction_force_scale));
const double moment_metric = NormalizedBalance(
moment_balance.Components(),
(std::max)(applied_moment_scale, reaction_moment_scale));
candidate.verification_metrics = {normalized_residual, force_metric,
moment_metric};
if (!IsFinite(candidate.equilibrium) ||
!Vector3{candidate.verification_metrics}.IsFinite()) {
return RecoveryFailure("nonfinite-recovery-value",
{domain.SourcePath(), 0U}, "global-equilibrium",
"Global equilibrium values and their physical "
"normalization scales must be finite.");
}
if (force_metric > kGlobalEquilibriumTolerance ||
moment_metric > kGlobalEquilibriumTolerance) {
return RecoveryFailure(
"global-equilibrium-tolerance-failure", {domain.SourcePath(), 0U},
"global-equilibrium",
"Normalized global force or moment balance exceeds 1e-10.");
}
return Status::Ok();
}
std::optional<AxisSet> LocalAxes(const Domain& domain,
const EntityIndex first_node,
const EntityIndex second_node,
@@ -562,67 +298,19 @@ Status ResultRecovery::Recover(const AnalysisModel& model,
return input_status;
}
Vector internal_force = full_stiffness.Multiply(displacement);
if (!IsFinite(internal_force)) {
return RecoveryFailure(
"nonfinite-recovery-value", {model.GetDomain().SourcePath(), 0U},
model.GetDomain().SourceContentIdentity(),
"Full stiffness multiplication must produce finite internal force.");
}
Vector residual{dofs.FullDofCount()};
for (std::size_t full_dof = 0U; full_dof < residual.Size(); ++full_dof) {
residual[full_dof] = internal_force[full_dof] - external_force[full_dof];
if (!std::isfinite(residual[full_dof])) {
return RecoveryFailure(
"nonfinite-recovery-value", {model.GetDomain().SourcePath(), 0U},
std::to_string(full_dof),
"Internal-minus-external residual must remain finite.");
}
}
const double residual_norm = IndexedNorm(residual, dofs.FreeDofs());
const auto internal_term_norms =
FreeEquationInternalTermNorms(full_stiffness, displacement, dofs);
const double external_norm = IndexedNorm(external_force, dofs.FreeDofs());
// Normalize against the three terms of
// Kff*df + Kfc*dc - Ff. Using only the already-cancelled K*d term would
// classify prescribed-only equilibrium roundoff as a unit residual.
const double denominator =
(std::max)({internal_term_norms[0U], internal_term_norms[1U],
external_norm});
if (!std::isfinite(residual_norm) || !std::isfinite(denominator)) {
return RecoveryFailure(
"nonfinite-recovery-value", {model.GetDomain().SourcePath(), 0U},
"free-residual",
"Free residual and its physical normalization scale must be finite.");
}
// An exact zero-load equilibrium is well-defined as zero. No unit floor is
// introduced; a nonzero residual with zero physical scale fails closed.
const double normalized_residual =
denominator == 0.0
? (residual_norm == 0.0 ? 0.0
: (std::numeric_limits<double>::infinity)())
: residual_norm / denominator;
if (!std::isfinite(normalized_residual) ||
normalized_residual > kFreeResidualTolerance) {
return RecoveryFailure(
"free-residual-tolerance-failure", {model.GetDomain().SourcePath(), 0U},
"free-residual", "The normalized free residual exceeds 1e-10.");
}
// The full-space reaction dataset preserves free residual evidence while
// its constrained entries are the physical reactions from K*d-F. Element
// end actions remain distinct output and are never re-summed here.
Vector reaction = residual;
std::vector<EndpointResultRow> endpoint_rows;
std::vector<GaussResultRow> gauss_rows;
std::vector<StressS11Row> stress_rows;
const Domain& domain = model.GetDomain();
ShellStateCandidate shell_candidate{};
std::vector<EntityIndex> expected_shell_elements;
bool has_beam_results = false;
bool has_shell_results = false;
auto global = results_internal::GlobalEquilibriumRecovery(
domain, dofs, full_stiffness, displacement, external_force);
if (!global.HasValue()) {
return global.GetStatus();
}
results_internal::RecoveryCandidate candidate{dofs.FullDofCount()};
candidate.displacement = displacement;
candidate.external_force = external_force;
candidate.internal_force = global.Value().internal_force;
candidate.residual = global.Value().residual;
candidate.reaction = global.Value().reaction;
for (std::size_t source_order = 0U; source_order < elements.size();
++source_order) {
const EntityIndex element_index = model.ActiveElements()[source_order];
@@ -641,8 +329,8 @@ Status ResultRecovery::Recover(const AnalysisModel& model,
if (!recovered.HasValue()) {
return recovered.GetStatus();
}
if (!SameSourceIdentity(recovered.Value().source_id,
element.DofLayout().source_id)) {
if (!results_internal::SameSourceIdentity(recovered.Value().source_id,
element.DofLayout().source_id)) {
return RecoveryFailure(
"invalid-element-result-identity", {domain.SourcePath(), 0U},
element.DofLayout().source_id.source_label_text,
@@ -653,16 +341,25 @@ Status ResultRecovery::Recover(const AnalysisModel& model,
[&](const auto& payload) -> Status {
using Payload = std::decay_t<decltype(payload)>;
if constexpr (std::is_same_v<Payload, BeamElementResultRows>) {
has_beam_results = true;
return AppendBeamRows(
const auto& node_indices = element.DofLayout().node_indices;
if (node_indices.size() != 2U ||
node_indices[0U] >= domain.Nodes().size() ||
node_indices[1U] >= domain.Nodes().size()) {
return results_internal::RecoveryFailure(
"invalid-element-result-identity", {domain.SourcePath(), 0U},
recovered.Value().source_id.source_label_text,
"Beam recovery requires exactly two valid source nodes.");
}
const std::array<SourceEntityId, 2> expected_nodes{
domain.Nodes()[node_indices[0U]].source_id,
domain.Nodes()[node_indices[1U]].source_id};
return results_internal::BeamResultRecovery(
{domain.SourcePath(), 0U}, recovered.Value().source_id,
element_index, payload, endpoint_rows, gauss_rows, stress_rows);
element_index, expected_nodes, payload, candidate);
} else {
has_shell_results = true;
return AppendShellRows({domain.SourcePath(), 0U},
recovered.Value().source_id, element_index,
payload, expected_shell_elements,
shell_candidate);
return results_internal::ShellResultRecovery(
{domain.SourcePath(), 0U}, recovered.Value().source_id,
element_index, payload, candidate);
}
},
recovered.Value().payload);
@@ -670,40 +367,21 @@ Status ResultRecovery::Recover(const AnalysisModel& model,
return aggregation_status;
}
}
if (has_beam_results && has_shell_results) {
if (candidate.has_beam_results && candidate.has_shell_results) {
return RecoveryFailure(
"unsupported-mixed-element-model", {domain.SourcePath(), 0U},
"mixed-runtime-results",
"Result recovery does not support mixed beam and shell models.");
}
if (has_shell_results) {
const Status evidence_status =
PopulateShellGlobalEvidence(domain, dofs, external_force, residual,
normalized_residual, shell_candidate);
if (candidate.has_shell_results) {
const Status evidence_status = results_internal::RecoverShellGlobalEvidence(
domain, dofs, external_force, global.Value(), candidate);
if (!evidence_status.IsOk()) {
return evidence_status;
}
}
// Build and validate a complete candidate state first. This preserves the
// prior full residual, beam rows, and shell rows if any later shell or
// candidate-inventory validation fails.
AnalysisState candidate_state = state;
candidate_state.Displacement() = displacement;
candidate_state.ExternalForce() = external_force;
candidate_state.InternalForce() = std::move(internal_force);
candidate_state.Residual() = std::move(residual);
candidate_state.Reaction() = std::move(reaction);
candidate_state.EndpointResults() = std::move(endpoint_rows);
candidate_state.GaussResults() = std::move(gauss_rows);
candidate_state.StressResults() = std::move(stress_rows);
const Status shell_commit_status = candidate_state.CommitShellResults(
expected_shell_elements, std::move(shell_candidate));
if (!shell_commit_status.IsOk()) {
return shell_commit_status;
}
state = std::move(candidate_state);
return Status::Ok();
return results_internal::CommitRecoveryCandidate(std::move(candidate), state);
}
Status ResultRecovery::Recover(const AnalysisModel& model,
@@ -774,7 +452,8 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations(
const EntityIndex node_index = definition.node_indices[endpoint];
if (node_index >= domain.Nodes().size() || row.element != element_index ||
row.endpoint != static_cast<int>(endpoint) ||
!SameSourceIdentity(row.node, domain.Nodes()[node_index].source_id)) {
!results_internal::SameSourceIdentity(
row.node, domain.Nodes()[node_index].source_id)) {
return RecoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-entity", definition.location,
definition.source_id.source_label_text,
@@ -0,0 +1,65 @@
#include "results/shell_result_recovery.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
namespace fesa::results_internal {
namespace {
bool IsFiniteShellRow(const ShellResultRow& row) {
if (!IsFiniteArray(row.natural_coordinates) ||
!IsFiniteArray(row.generalized_strain) ||
!IsFiniteArray(row.section_resultant)) {
return false;
}
for (const auto& axis : row.local_frame) {
if (!IsFiniteArray(axis)) {
return false;
}
}
return std::all_of(row.stress.begin(), row.stress.end(),
[](const ShellSectionStressRow& stress) {
return std::isfinite(stress.zeta) &&
IsFiniteArray(stress.components);
});
}
} // namespace
Status ShellResultRecovery(const SourceLocation& location,
const SourceEntityId& source_id,
const EntityIndex expected_element,
const ShellElementResultRows& rows,
RecoveryCandidate& candidate) {
const double accumulated_energy =
candidate.shell.physical_strain_energy + rows.physical_strain_energy;
if (!std::isfinite(rows.physical_strain_energy) ||
!std::isfinite(accumulated_energy)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Source-order physical shell energy reduction must remain finite.");
}
for (const auto& row : rows.rows) {
if (row.element != expected_element) {
return RecoveryFailure(
"invalid-element-result-identity", location,
source_id.source_label_text,
"Shell rows must retain their active element identity.");
}
if (!IsFiniteShellRow(row)) {
return RecoveryFailure(
"nonfinite-recovery-value", location, source_id.source_label_text,
"Shell physical recovery rows must remain finite.");
}
}
candidate.has_shell_results = true;
candidate.shell.rows.insert(candidate.shell.rows.end(), rows.rows.begin(),
rows.rows.end());
candidate.expected_shell_elements.push_back(expected_element);
candidate.shell.physical_strain_energy = accumulated_energy;
return Status::Ok();
}
} // namespace fesa::results_internal
+20
View File
@@ -0,0 +1,20 @@
#ifndef FESA_RESULTS_SHELL_RESULT_RECOVERY_H_
#define FESA_RESULTS_SHELL_RESULT_RECOVERY_H_
#include "fesa/core/source_identity.h"
#include "fesa/core/status.h"
#include "fesa/elements/element.h"
#include "results/recovery_candidate.h"
namespace fesa::results_internal {
/// @brief Validates and appends physical shell rows and energy evidence.
Status ShellResultRecovery(const SourceLocation& location,
const SourceEntityId& source_id,
EntityIndex expected_element,
const ShellElementResultRows& rows,
RecoveryCandidate& candidate);
} // namespace fesa::results_internal
#endif // FESA_RESULTS_SHELL_RESULT_RECOVERY_H_
+1
View File
@@ -37,6 +37,7 @@ add_executable(
unit/materials/material_test.cpp
unit/properties/element_property_test.cpp
unit/results/result_records_test.cpp
unit/results/result_recovery_components_test.cpp
unit/results/result_recovery_test.cpp
unit/results/results_writer_test.cpp
unit/solvers/linear/linear_solver_test.cpp
@@ -0,0 +1,593 @@
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include <cstddef>
#include <filesystem>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/analysis_state.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/assembly/sparse_assembler.h"
#include "fesa/elements/element.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/vector3.h"
#include "fesa/model/domain.h"
#include "results/analysis_state_commit.h"
#include "results/beam_result_recovery.h"
#include "results/global_equilibrium_recovery.h"
#include "results/recovery_candidate.h"
#include "results/shell_result_recovery.h"
namespace {
struct ShellFixture {
std::unique_ptr<fesa::Domain> domain;
std::unique_ptr<fesa::AnalysisModel> model;
std::unique_ptr<fesa::DofManager> dofs;
std::unique_ptr<fesa::SparseMatrix> stiffness;
};
fesa::SourceEntityId MakeSourceId(const std::string& text) {
return {"Part-1", 10, text};
}
fesa::BeamElementResultRows MakeBeamRows(const fesa::EntityIndex element,
const double value) {
fesa::BeamElementResultRows rows{};
rows.endpoint_rows = {{element,
0,
MakeSourceId("1"),
{-value, 0.0, 0.0, 0.0, 0.0, 0.0},
{value, 2.0 * value, 3.0 * value, 4.0 * value}},
{element,
1,
MakeSourceId("2"),
{value, 0.0, 0.0, 0.0, 0.0, 0.0},
{value, 2.0 * value, 3.0 * value, 4.0 * value}}};
rows.gauss_rows = {{element,
1,
{value, value + 1.0, value + 2.0, value + 3.0},
{value, 2.0 * value, 3.0 * value, 4.0 * value}},
{element,
2,
{value + 4.0, value + 5.0, value + 6.0, value + 7.0},
{value + 8.0, value + 9.0, value + 10.0, value + 11.0}}};
rows.stress_rows = {{element, 1, 0U, 0.0, 0.0, 5.0 * value, "fake"},
{element, 2, 1U, -0.25, 0.5, 6.0 * value, "input"}};
return rows;
}
fesa::ShellElementResultRows MakeShellRows(const fesa::EntityIndex element,
const double physical_energy) {
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<fesa::ShellMidsurfaceLocation, 4> locations{
fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2,
fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4};
const std::array<std::array<double, 2>, 4> coordinates{
std::array<double, 2>{-gauss, -gauss},
std::array<double, 2>{gauss, -gauss}, std::array<double, 2>{gauss, gauss},
std::array<double, 2>{-gauss, gauss}};
const std::array<fesa::ShellSectionPosition, 3> positions{
fesa::ShellSectionPosition::kBottom, fesa::ShellSectionPosition::kMiddle,
fesa::ShellSectionPosition::kTop};
constexpr std::array<double, 3> zeta{-1.0, 0.0, 1.0};
fesa::ShellElementResultRows rows{};
rows.physical_strain_energy = physical_energy;
for (std::size_t point = 0U; point < locations.size(); ++point) {
fesa::ShellResultRow row{};
row.element = element;
row.location = locations[point];
row.natural_coordinates = coordinates[point];
row.local_frame = {{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
for (std::size_t component = 0U; component < row.generalized_strain.size();
++component) {
row.generalized_strain[component] =
static_cast<double>(point + component + 1U);
row.section_resultant[component] =
10.0 * static_cast<double>(point + component + 1U);
}
for (std::size_t position = 0U; position < positions.size(); ++position) {
row.stress[position] = {positions[position],
zeta[position],
{static_cast<double>(point + position + 1U),
static_cast<double>(point + position + 2U),
static_cast<double>(point + position + 3U)}};
}
rows.rows.push_back(std::move(row));
}
return rows;
}
fesa::ModelDefinition MakeShellDefinition() {
const std::filesystem::path source{"models/recovery-components-shell.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "fnv1a64:1234567890abcdef";
definition.nodes = {{{"Shell-1", 1, "1"}, {-1.0, -1.0, 0.0}, {source, 10U}},
{{"Shell-1", 2, "2"}, {1.0, -1.0, 0.0}, {source, 11U}},
{{"Shell-1", 3, "3"}, {1.0, 1.0, 0.0}, {source, 12U}},
{{"Shell-1", 4, "4"}, {-1.0, 1.0, 0.0}, {source, 13U}}};
definition.materials = {{"Material", 120.0, 0.25, {source, 20U}}};
definition.shell_sections = {{"ShellSection", 2.0, 0U, {source, 30U}}};
definition.shell_elements = {{{"Shell-1", 10, "10"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 40U}}};
for (std::size_t node = 0U; node < definition.nodes.size(); ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}};
return definition;
}
ShellFixture MakeShellFixture(fesa::ModelDefinition definition) {
auto domain_result = fesa::Domain::Create(std::move(definition));
if (!domain_result.HasValue()) {
throw std::runtime_error{"Shell Domain construction failed."};
}
auto domain =
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model_result = fesa::AnalysisModel::Create(*domain);
if (!model_result.HasValue()) {
throw std::runtime_error{"Shell AnalysisModel construction failed."};
}
auto model =
std::make_unique<fesa::AnalysisModel>(std::move(model_result.Value()));
auto dofs_result = fesa::DofManager::Create(*model);
if (!dofs_result.HasValue()) {
throw std::runtime_error{"Shell DofManager construction failed."};
}
auto dofs =
std::make_unique<fesa::DofManager>(std::move(dofs_result.Value()));
fesa::SerialParallelFor serial;
auto stiffness_result =
fesa::SparseAssembler::AssembleStiffness(*model, *dofs, serial);
if (!stiffness_result.HasValue()) {
throw std::runtime_error{"Shell stiffness assembly failed."};
}
auto stiffness =
std::make_unique<fesa::SparseMatrix>(std::move(stiffness_result.Value()));
return {std::move(domain), std::move(model), std::move(dofs),
std::move(stiffness)};
}
fesa::AnalysisState MakeShellPhysicalState(const ShellFixture& fixture) {
constexpr std::array<double, 8> generalized{0.1, -0.05, 0.2, 0.3,
-0.15, 0.25, 0.4, -0.3};
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
for (std::size_t node = 0U; node < fixture.domain->Nodes().size(); ++node) {
const double x = fixture.domain->Nodes()[node].coordinates[0U];
const double y = fixture.domain->Nodes()[node].coordinates[1U];
const std::size_t offset = 6U * node;
state.Displacement()[offset] =
generalized[0U] * x + 0.5 * generalized[2U] * y;
state.Displacement()[offset + 1U] =
generalized[1U] * y + 0.5 * generalized[2U] * x;
state.Displacement()[offset + 2U] = generalized[6U] * x +
generalized[7U] * y -
0.5 * generalized[5U] * x * y;
state.Displacement()[offset + 3U] =
-generalized[4U] * y - 0.5 * generalized[5U] * x;
state.Displacement()[offset + 4U] =
generalized[3U] * x + 0.5 * generalized[5U] * y;
}
state.ExternalForce() = fixture.stiffness->Multiply(state.Displacement());
return state;
}
void ExpectStatusCode(const fesa::Status& status, const std::string& code) {
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel);
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, code);
}
void ExpectVectorEqual(const fesa::Vector& actual,
const fesa::Vector& expected) {
ASSERT_EQ(actual.Size(), expected.Size());
for (std::size_t index = 0U; index < actual.Size(); ++index) {
EXPECT_DOUBLE_EQ(actual[index], expected[index]);
}
}
void ExpectAnalysisStateEqual(const fesa::AnalysisState& actual,
const fesa::AnalysisState& expected) {
EXPECT_EQ(actual.Identity().step_name, expected.Identity().step_name);
EXPECT_EQ(actual.Identity().frame_index, expected.Identity().frame_index);
ExpectVectorEqual(actual.Displacement(), expected.Displacement());
ExpectVectorEqual(actual.ExternalForce(), expected.ExternalForce());
ExpectVectorEqual(actual.InternalForce(), expected.InternalForce());
ExpectVectorEqual(actual.Residual(), expected.Residual());
ExpectVectorEqual(actual.Reaction(), expected.Reaction());
ASSERT_EQ(actual.EndpointResults().size(), expected.EndpointResults().size());
for (std::size_t row = 0U; row < actual.EndpointResults().size(); ++row) {
EXPECT_EQ(actual.EndpointResults()[row].element,
expected.EndpointResults()[row].element);
EXPECT_EQ(actual.EndpointResults()[row].endpoint,
expected.EndpointResults()[row].endpoint);
EXPECT_EQ(actual.EndpointResults()[row].node.instance_name,
expected.EndpointResults()[row].node.instance_name);
EXPECT_EQ(actual.EndpointResults()[row].node.source_label,
expected.EndpointResults()[row].node.source_label);
EXPECT_EQ(actual.EndpointResults()[row].node.source_label_text,
expected.EndpointResults()[row].node.source_label_text);
EXPECT_EQ(actual.EndpointResults()[row].end_action,
expected.EndpointResults()[row].end_action);
EXPECT_EQ(actual.EndpointResults()[row].section_resultant,
expected.EndpointResults()[row].section_resultant);
}
ASSERT_EQ(actual.GaussResults().size(), expected.GaussResults().size());
for (std::size_t row = 0U; row < actual.GaussResults().size(); ++row) {
EXPECT_EQ(actual.GaussResults()[row].element,
expected.GaussResults()[row].element);
EXPECT_EQ(actual.GaussResults()[row].gauss_point,
expected.GaussResults()[row].gauss_point);
EXPECT_EQ(actual.GaussResults()[row].generalized_strain,
expected.GaussResults()[row].generalized_strain);
EXPECT_EQ(actual.GaussResults()[row].generalized_resultant,
expected.GaussResults()[row].generalized_resultant);
}
ASSERT_EQ(actual.StressResults().size(), expected.StressResults().size());
for (std::size_t row = 0U; row < actual.StressResults().size(); ++row) {
EXPECT_EQ(actual.StressResults()[row].element,
expected.StressResults()[row].element);
EXPECT_EQ(actual.StressResults()[row].gauss_point,
expected.StressResults()[row].gauss_point);
EXPECT_EQ(actual.StressResults()[row].section_point,
expected.StressResults()[row].section_point);
EXPECT_DOUBLE_EQ(actual.StressResults()[row].x1,
expected.StressResults()[row].x1);
EXPECT_DOUBLE_EQ(actual.StressResults()[row].x2,
expected.StressResults()[row].x2);
EXPECT_DOUBLE_EQ(actual.StressResults()[row].s11,
expected.StressResults()[row].s11);
EXPECT_EQ(actual.StressResults()[row].source,
expected.StressResults()[row].source);
}
ASSERT_EQ(actual.ShellResults().size(), expected.ShellResults().size());
for (std::size_t row = 0U; row < actual.ShellResults().size(); ++row) {
EXPECT_EQ(actual.ShellResults()[row].element,
expected.ShellResults()[row].element);
EXPECT_EQ(actual.ShellResults()[row].location,
expected.ShellResults()[row].location);
EXPECT_EQ(actual.ShellResults()[row].natural_coordinates,
expected.ShellResults()[row].natural_coordinates);
EXPECT_EQ(actual.ShellResults()[row].local_frame,
expected.ShellResults()[row].local_frame);
EXPECT_EQ(actual.ShellResults()[row].generalized_strain,
expected.ShellResults()[row].generalized_strain);
EXPECT_EQ(actual.ShellResults()[row].section_resultant,
expected.ShellResults()[row].section_resultant);
}
EXPECT_DOUBLE_EQ(actual.PhysicalStrainEnergy(),
expected.PhysicalStrainEnergy());
EXPECT_EQ(actual.Equilibrium(), expected.Equilibrium());
EXPECT_EQ(actual.VerificationMetrics(), expected.VerificationMetrics());
}
} // namespace
// C-MODULE-002
TEST(ResultRecoveryComponents,
GlobalEquilibriumUsesFullResidualAndGlobalOriginMoment) {
auto centered_definition = MakeShellDefinition();
auto translated_definition = centered_definition;
constexpr fesa::Vector3 translation{7.0, 11.0, 0.0};
for (auto& node : translated_definition.nodes) {
for (std::size_t component = 0U;
component < translation.Components().size(); ++component) {
node.coordinates[component] += translation[component];
}
}
const auto centered_fixture =
MakeShellFixture(std::move(centered_definition));
const auto translated_fixture =
MakeShellFixture(std::move(translated_definition));
auto centered = MakeShellPhysicalState(centered_fixture);
auto translated = MakeShellPhysicalState(translated_fixture);
centered.ExternalForce()[0U] += 1.0e-9;
translated.ExternalForce()[0U] += 1.0e-9;
auto centered_global = fesa::results_internal::GlobalEquilibriumRecovery(
*centered_fixture.domain, *centered_fixture.dofs,
*centered_fixture.stiffness, centered.Displacement(),
centered.ExternalForce());
ASSERT_TRUE(centered_global.HasValue());
auto translated_global = fesa::results_internal::GlobalEquilibriumRecovery(
*translated_fixture.domain, *translated_fixture.dofs,
*translated_fixture.stiffness, translated.Displacement(),
translated.ExternalForce());
ASSERT_TRUE(translated_global.HasValue());
EXPECT_NEAR(centered_global.Value().residual[0U], -1.0e-9, 1.0e-14);
EXPECT_DOUBLE_EQ(centered_global.Value().reaction[0U],
centered_global.Value().residual[0U]);
fesa::results_internal::RecoveryCandidate centered_candidate{
centered_fixture.dofs->FullDofCount()};
const auto centered_status =
fesa::results_internal::RecoverShellGlobalEvidence(
*centered_fixture.domain, *centered_fixture.dofs,
centered.ExternalForce(), centered_global.Value(),
centered_candidate);
ASSERT_TRUE(centered_status.IsOk());
fesa::results_internal::RecoveryCandidate translated_candidate{
translated_fixture.dofs->FullDofCount()};
const auto translated_status =
fesa::results_internal::RecoverShellGlobalEvidence(
*translated_fixture.domain, *translated_fixture.dofs,
translated.ExternalForce(), translated_global.Value(),
translated_candidate);
ASSERT_TRUE(translated_status.IsOk());
std::array<double, 3> centered_force{};
for (std::size_t component = 0U; component < centered_force.size();
++component) {
centered_force[component] = centered_candidate.shell.equilibrium[component];
EXPECT_NEAR(translated_candidate.shell.equilibrium[component],
centered_force[component], 1.0e-12);
}
const fesa::Vector3 translated_moment_delta =
translation.Cross(fesa::Vector3{centered_force});
for (std::size_t component = 0U; component < centered_force.size();
++component) {
EXPECT_NEAR(translated_candidate.shell.equilibrium[3U + component] -
centered_candidate.shell.equilibrium[3U + component],
translated_moment_delta[component], 5.0e-12);
}
}
// C-MODULE-002
TEST(ResultRecoveryComponents,
BeamResultRecoveryKeepsEndpointGaussAndStressIdentities) {
constexpr fesa::EntityIndex element = 7U;
const std::array<fesa::SourceEntityId, 2> expected_nodes{MakeSourceId("1"),
MakeSourceId("2")};
fesa::results_internal::RecoveryCandidate candidate{0U};
const auto status = fesa::results_internal::BeamResultRecovery(
{}, MakeSourceId("10"), element, expected_nodes,
MakeBeamRows(element, 2.0), candidate);
ASSERT_TRUE(status.IsOk());
ASSERT_EQ(candidate.endpoint_rows.size(), 2U);
ASSERT_EQ(candidate.gauss_rows.size(), 2U);
ASSERT_EQ(candidate.stress_rows.size(), 2U);
EXPECT_TRUE(candidate.has_beam_results);
EXPECT_EQ(candidate.endpoint_rows[0U].endpoint, 0);
EXPECT_EQ(candidate.endpoint_rows[1U].endpoint, 1);
EXPECT_DOUBLE_EQ(candidate.endpoint_rows[0U].end_action[0U], -2.0);
EXPECT_DOUBLE_EQ(candidate.endpoint_rows[0U].section_resultant[0U], 2.0);
EXPECT_DOUBLE_EQ(candidate.endpoint_rows[1U].end_action[0U], 2.0);
EXPECT_DOUBLE_EQ(candidate.endpoint_rows[1U].section_resultant[0U], 2.0);
EXPECT_EQ(candidate.gauss_rows[0U].gauss_point, 1);
EXPECT_EQ(candidate.gauss_rows[1U].gauss_point, 2);
EXPECT_EQ(candidate.stress_rows[0U].section_point, 0U);
EXPECT_EQ(candidate.stress_rows[1U].section_point, 1U);
auto invalid = MakeBeamRows(element, 3.0);
invalid.stress_rows[1U].s11 = std::numeric_limits<double>::quiet_NaN();
fesa::results_internal::RecoveryCandidate rejected{0U};
ExpectStatusCode(
fesa::results_internal::BeamResultRecovery(
{}, MakeSourceId("10"), element, expected_nodes, invalid, rejected),
"nonfinite-recovery-value");
EXPECT_TRUE(rejected.endpoint_rows.empty());
EXPECT_TRUE(rejected.gauss_rows.empty());
EXPECT_TRUE(rejected.stress_rows.empty());
}
// C-MODULE-002
TEST(ResultRecoveryComponents,
BeamResultRecoveryRejectsMalformedB33IdentityBeforeAppending) {
constexpr fesa::EntityIndex element = 7U;
const std::array<fesa::SourceEntityId, 2> expected_nodes{MakeSourceId("1"),
MakeSourceId("2")};
std::vector<std::pair<std::string, fesa::BeamElementResultRows>> invalid;
auto wrong_endpoint = MakeBeamRows(element, 1.0);
wrong_endpoint.endpoint_rows[1U].endpoint = 2;
invalid.emplace_back("wrong endpoint", std::move(wrong_endpoint));
auto swapped_endpoints = MakeBeamRows(element, 1.0);
std::swap(swapped_endpoints.endpoint_rows[0U],
swapped_endpoints.endpoint_rows[1U]);
invalid.emplace_back("swapped endpoints", std::move(swapped_endpoints));
auto missing_endpoint = MakeBeamRows(element, 1.0);
missing_endpoint.endpoint_rows.pop_back();
invalid.emplace_back("missing endpoint", std::move(missing_endpoint));
auto duplicate_endpoint = MakeBeamRows(element, 1.0);
duplicate_endpoint.endpoint_rows[1U] = duplicate_endpoint.endpoint_rows[0U];
invalid.emplace_back("duplicate endpoint", std::move(duplicate_endpoint));
auto wrong_source_node = MakeBeamRows(element, 1.0);
wrong_source_node.endpoint_rows[0U].node = MakeSourceId("99");
invalid.emplace_back("wrong source node", std::move(wrong_source_node));
auto wrong_gauss = MakeBeamRows(element, 1.0);
wrong_gauss.gauss_rows[1U].gauss_point = 3;
invalid.emplace_back("wrong GP", std::move(wrong_gauss));
auto swapped_gauss = MakeBeamRows(element, 1.0);
std::swap(swapped_gauss.gauss_rows[0U], swapped_gauss.gauss_rows[1U]);
invalid.emplace_back("swapped GP", std::move(swapped_gauss));
auto missing_gauss = MakeBeamRows(element, 1.0);
missing_gauss.gauss_rows.pop_back();
invalid.emplace_back("missing GP", std::move(missing_gauss));
auto duplicate_gauss = MakeBeamRows(element, 1.0);
duplicate_gauss.gauss_rows[1U] = duplicate_gauss.gauss_rows[0U];
invalid.emplace_back("duplicate GP", std::move(duplicate_gauss));
auto malformed_stress_order = MakeBeamRows(element, 1.0);
std::swap(malformed_stress_order.stress_rows[0U],
malformed_stress_order.stress_rows[1U]);
invalid.emplace_back("malformed stress order",
std::move(malformed_stress_order));
for (const auto& [description, rows] : invalid) {
SCOPED_TRACE(description);
fesa::results_internal::RecoveryCandidate candidate{0U};
ExpectStatusCode(
fesa::results_internal::BeamResultRecovery(
{}, MakeSourceId("10"), element, expected_nodes, rows, candidate),
"invalid-element-result-identity");
EXPECT_TRUE(candidate.endpoint_rows.empty());
EXPECT_TRUE(candidate.gauss_rows.empty());
EXPECT_TRUE(candidate.stress_rows.empty());
EXPECT_FALSE(candidate.has_beam_results);
}
}
// C-MODULE-002
TEST(ResultRecoveryComponents,
ShellResultRecoveryKeepsGpStressOrderAndPhysicalEnergy) {
constexpr fesa::EntityIndex element = 4U;
fesa::results_internal::RecoveryCandidate candidate{0U};
const auto status = fesa::results_internal::ShellResultRecovery(
{}, MakeSourceId("20"), element, MakeShellRows(element, 12.5), candidate);
ASSERT_TRUE(status.IsOk());
EXPECT_TRUE(candidate.has_shell_results);
EXPECT_EQ(candidate.expected_shell_elements,
(std::vector<fesa::EntityIndex>{element}));
ASSERT_EQ(candidate.shell.rows.size(), 4U);
EXPECT_EQ(candidate.shell.rows[0U].location,
fesa::ShellMidsurfaceLocation::kGp1);
EXPECT_EQ(candidate.shell.rows[3U].location,
fesa::ShellMidsurfaceLocation::kGp4);
EXPECT_EQ(candidate.shell.rows[0U].stress[0U].position,
fesa::ShellSectionPosition::kBottom);
EXPECT_EQ(candidate.shell.rows[0U].stress[1U].position,
fesa::ShellSectionPosition::kMiddle);
EXPECT_EQ(candidate.shell.rows[0U].stress[2U].position,
fesa::ShellSectionPosition::kTop);
EXPECT_DOUBLE_EQ(candidate.shell.physical_strain_energy, 12.5);
auto invalid = MakeShellRows(element, 12.5);
invalid.rows[2U].element = element + 1U;
fesa::results_internal::RecoveryCandidate rejected{0U};
ExpectStatusCode(fesa::results_internal::ShellResultRecovery(
{}, MakeSourceId("20"), element, invalid, rejected),
"invalid-element-result-identity");
EXPECT_TRUE(rejected.shell.rows.empty());
EXPECT_TRUE(rejected.expected_shell_elements.empty());
EXPECT_DOUBLE_EQ(rejected.shell.physical_strain_energy, 0.0);
}
// C-MODULE-002
TEST(ResultRecoveryComponents,
CommitRecoveryCandidateRollsBackLaterInvalidShellBundle) {
const auto fixture = MakeShellFixture(MakeShellDefinition());
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
for (std::size_t full_dof = 0U; full_dof < fixture.dofs->FullDofCount();
++full_dof) {
state.Displacement()[full_dof] = static_cast<double>(full_dof) + 0.25;
state.ExternalForce()[full_dof] = -static_cast<double>(full_dof) - 0.5;
state.InternalForce()[full_dof] = 100.0 + static_cast<double>(full_dof);
state.Residual()[full_dof] = 200.0 + static_cast<double>(full_dof);
state.Reaction()[full_dof] = 300.0 + static_cast<double>(full_dof);
}
state.EndpointResults() = MakeBeamRows(0U, 9.0).endpoint_rows;
auto prior_shell_rows = MakeShellRows(0U, 71.0);
fesa::ShellStateCandidate prior_shell{};
prior_shell.rows = prior_shell_rows.rows;
prior_shell.physical_strain_energy = prior_shell_rows.physical_strain_energy;
prior_shell.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
prior_shell.verification_metrics = {1.0e-11, 2.0e-11, 3.0e-11};
ASSERT_TRUE(state.CommitShellResults({0U}, std::move(prior_shell)).IsOk());
const fesa::AnalysisState prior = state;
fesa::results_internal::RecoveryCandidate candidate{
fixture.dofs->FullDofCount()};
for (std::size_t full_dof = 0U; full_dof < fixture.dofs->FullDofCount();
++full_dof) {
candidate.displacement[full_dof] = 10.0 + static_cast<double>(full_dof);
candidate.external_force[full_dof] = 20.0 + static_cast<double>(full_dof);
candidate.internal_force[full_dof] = 30.0 + static_cast<double>(full_dof);
candidate.residual[full_dof] = 40.0 + static_cast<double>(full_dof);
candidate.reaction[full_dof] = 50.0 + static_cast<double>(full_dof);
}
candidate.has_shell_results = true;
candidate.expected_shell_elements = {0U};
auto invalid_shell_rows = MakeShellRows(0U, 12.5);
candidate.shell.rows = invalid_shell_rows.rows;
candidate.shell.rows[3U].location = fesa::ShellMidsurfaceLocation::kGp1;
candidate.shell.physical_strain_energy =
invalid_shell_rows.physical_strain_energy;
candidate.shell.equilibrium = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
candidate.shell.verification_metrics = {0.0, 0.0, 0.0};
const auto status = fesa::results_internal::CommitRecoveryCandidate(
std::move(candidate), state);
ExpectStatusCode(status, "invalid-shell-state-inventory");
ExpectVectorEqual(state.Displacement(), prior.Displacement());
ExpectVectorEqual(state.ExternalForce(), prior.ExternalForce());
ExpectVectorEqual(state.InternalForce(), prior.InternalForce());
ExpectVectorEqual(state.Residual(), prior.Residual());
ExpectVectorEqual(state.Reaction(), prior.Reaction());
ASSERT_EQ(state.EndpointResults().size(), prior.EndpointResults().size());
EXPECT_EQ(state.EndpointResults()[0U].end_action,
prior.EndpointResults()[0U].end_action);
ASSERT_EQ(state.ShellResults().size(), prior.ShellResults().size());
EXPECT_EQ(state.ShellResults()[0U].location,
prior.ShellResults()[0U].location);
EXPECT_EQ(state.ShellResults()[0U].section_resultant,
prior.ShellResults()[0U].section_resultant);
EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), prior.PhysicalStrainEnergy());
EXPECT_EQ(state.Equilibrium(), prior.Equilibrium());
EXPECT_EQ(state.VerificationMetrics(), prior.VerificationMetrics());
}
// C-MODULE-002
TEST(ResultRecoveryComponents,
CommitRecoveryCandidateRejectsNonfiniteVectorAndPreservesWholeState) {
const auto fixture = MakeShellFixture(MakeShellDefinition());
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Prior-Step", 3U});
for (std::size_t full_dof = 0U; full_dof < fixture.dofs->FullDofCount();
++full_dof) {
state.Displacement()[full_dof] = static_cast<double>(full_dof) + 0.25;
state.ExternalForce()[full_dof] = -static_cast<double>(full_dof) - 0.5;
state.InternalForce()[full_dof] = 100.0 + static_cast<double>(full_dof);
state.Residual()[full_dof] = 200.0 + static_cast<double>(full_dof);
state.Reaction()[full_dof] = 300.0 + static_cast<double>(full_dof);
}
const auto prior_beam = MakeBeamRows(0U, 9.0);
state.EndpointResults() = prior_beam.endpoint_rows;
state.GaussResults() = prior_beam.gauss_rows;
state.StressResults() = prior_beam.stress_rows;
auto prior_shell_rows = MakeShellRows(0U, 71.0);
fesa::ShellStateCandidate prior_shell{};
prior_shell.rows = prior_shell_rows.rows;
prior_shell.physical_strain_energy = prior_shell_rows.physical_strain_energy;
prior_shell.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
prior_shell.verification_metrics = {1.0e-11, 2.0e-11, 3.0e-11};
ASSERT_TRUE(state.CommitShellResults({0U}, std::move(prior_shell)).IsOk());
const fesa::AnalysisState prior = state;
fesa::results_internal::RecoveryCandidate candidate{
fixture.dofs->FullDofCount()};
candidate.displacement[0U] = 10.0;
candidate.external_force[0U] = 20.0;
candidate.internal_force[0U] = 30.0;
candidate.residual[0U] = 40.0;
candidate.reaction[0U] = std::numeric_limits<double>::quiet_NaN();
const auto status = fesa::results_internal::CommitRecoveryCandidate(
std::move(candidate), state);
ExpectStatusCode(status, "nonfinite-recovery-value");
ExpectAnalysisStateEqual(state, prior);
}