feat(linear-static-3d-euler-beam): step 22 - result-recovery

This commit is contained in:
KOKO\Mimi
2026-08-09 21:15:25 +09:00
parent df84903745
commit 084e6b0be1
6 changed files with 1358 additions and 0 deletions
+1
View File
@@ -18,6 +18,7 @@ add_library(
math/sparse_matrix.cpp
math/vector.cpp
model/domain.cpp
results/result_recovery.cpp
solvers/linear/mkl_pardiso_solver.cpp
)
+740
View File
@@ -0,0 +1,740 @@
#include "fesa/results/result_recovery.hpp"
#include "fesa/elements/euler_beam_3d.hpp"
#include <algorithm>
#include <array>
#include <charconv>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <stdexcept>
#include <string>
#include <system_error>
#include <utility>
#include <vector>
namespace fesa {
namespace {
constexpr std::size_t kDofsPerNode = 6U;
constexpr std::size_t kElementDofCount = 12U;
constexpr double kFreeResidualTolerance = 1.0e-10;
constexpr double kAxisTolerance = 1.0e-12;
using AxisSet = std::array<std::array<double, 3>, 3>;
Status recoveryFailure(const std::string& code,
const SourceLocation& location,
const std::string& identity,
const std::string& message) {
return Status::failure(
FailureCategory::model,
{{Severity::error,
code,
location,
"RESULT_RECOVERY",
identity,
message}});
}
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));
}
bool sameSourceIdentity(const SourceEntityId& left,
const SourceEntityId& right) {
return left.instanceName == right.instanceName &&
left.sourceLabel == right.sourceLabel &&
left.sourceLabelText == right.sourceLabelText;
}
bool finite(const std::array<double, 4>& values) {
return std::all_of(
values.begin(), values.end(),
[](const double value) { return std::isfinite(value); });
}
bool finite(const std::array<double, 6>& values) {
return std::all_of(
values.begin(), values.end(),
[](const double value) { return std::isfinite(value); });
}
bool finite(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;
}
bool strictlyIncreasing(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 validateRecoveryInputs(const AnalysisModel& model,
const DofManager& dofs,
const SparseMatrix& fullStiffness,
const AnalysisState& state) {
const Domain& domain = model.domain();
if (domain.nodes().size() >
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode) {
return recoveryFailure(
"invalid-recovery-dimensions",
{domain.sourcePath(), 0U},
domain.sourceContentIdentity(),
"The semantic node count cannot be represented in full-DOF space.");
}
const std::size_t fullCount = domain.nodes().size() * kDofsPerNode;
if (dofs.fullDofCount() != fullCount ||
fullStiffness.rows() != fullCount ||
fullStiffness.columns() != fullCount ||
state.displacement().size() != fullCount ||
state.externalForce().size() != fullCount ||
state.internalForce().size() != fullCount ||
state.residual().size() != fullCount ||
state.reaction().size() != fullCount) {
return recoveryFailure(
"invalid-recovery-dimensions",
{domain.sourcePath(), 0U},
domain.sourceContentIdentity(),
"Model, DOF, stiffness, and AnalysisState full-space dimensions must agree.");
}
const Status matrixStatus = fullStiffness.validate();
if (!matrixStatus.isOk()) {
return matrixStatus;
}
if (!finite(state.displacement()) || !finite(state.externalForce())) {
return recoveryFailure(
"nonfinite-recovery-value",
{domain.sourcePath(), 0U},
domain.sourceContentIdentity(),
"Displacement and external-force inputs must be finite.");
}
const auto& freeDofs = dofs.freeDofs();
const auto& constrainedDofs = dofs.constrainedDofs();
if (freeDofs.size() != dofs.freeDofCount() ||
constrainedDofs.size() != dofs.constrainedDofCount() ||
dofs.prescribedValues().size() != constrainedDofs.size() ||
freeDofs.size() + constrainedDofs.size() != fullCount ||
!strictlyIncreasing(freeDofs) ||
!strictlyIncreasing(constrainedDofs)) {
return recoveryFailure(
"invalid-recovery-order",
{domain.sourcePath(), 0U},
domain.sourceContentIdentity(),
"Free and constrained DOFs must form stable increasing full-space orders.");
}
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 recoveryFailure(
"invalid-recovery-order",
{domain.sourcePath(), 0U},
std::to_string(fullDof),
"Free equations must match stable full-DOF order.");
}
ownership[fullDof] = 1U;
}
for (std::size_t constrained = 0U;
constrained < constrainedDofs.size();
++constrained) {
const std::size_t fullDof = constrainedDofs[constrained];
if (fullDof >= fullCount || ownership[fullDof] != 0U ||
dofs.freeEquation(fullDof).has_value()) {
return recoveryFailure(
"invalid-recovery-order",
{domain.sourcePath(), 0U},
std::to_string(fullDof),
"Constrained DOFs must be unique and absent from free equations.");
}
if (!std::isfinite(dofs.prescribedValues()[constrained]) ||
state.displacement()[fullDof] !=
dofs.prescribedValues()[constrained]) {
return recoveryFailure(
"invalid-recovery-state",
{domain.sourcePath(), 0U},
std::to_string(fullDof),
"Constrained displacement must equal its prescribed value before recovery.");
}
ownership[fullDof] = 2U;
}
} catch (const std::out_of_range&) {
return recoveryFailure(
"invalid-recovery-dimensions",
{domain.sourcePath(), 0U},
domain.sourceContentIdentity(),
"DofManager equation storage must cover every full DOF.");
}
if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) {
return recoveryFailure(
"invalid-recovery-order",
{domain.sourcePath(), 0U},
domain.sourceContentIdentity(),
"Free and constrained DOFs must partition the full range.");
}
EntityIndex previousElement = 0U;
bool firstElement = true;
for (const EntityIndex element : model.activeElements()) {
if (element >= domain.elements().size() ||
(!firstElement && element <= previousElement)) {
return recoveryFailure(
"invalid-recovery-entity",
{domain.sourcePath(), 0U},
std::to_string(element),
"Active elements must be unique in stable internal-index order.");
}
firstElement = false;
previousElement = element;
const auto& definition = domain.elements()[element];
if (definition.nodeIndices[0U] >= domain.nodes().size() ||
definition.nodeIndices[1U] >= domain.nodes().size() ||
definition.materialIndex >= domain.materials().size() ||
definition.sectionIndex >= domain.sections().size()) {
return recoveryFailure(
"invalid-recovery-entity",
definition.location,
definition.sourceId.sourceLabelText,
"Active beam references must resolve before recovery.");
}
try {
const auto& scatter = dofs.elementScatter(element);
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
for (std::size_t component = 0U;
component < kDofsPerNode;
++component) {
const std::size_t expected =
static_cast<std::size_t>(definition.nodeIndices[endpoint]) *
kDofsPerNode +
component;
if (scatter[endpoint * kDofsPerNode + component] != expected ||
expected >= fullCount) {
return recoveryFailure(
"invalid-recovery-order",
definition.location,
definition.sourceId.sourceLabelText,
"Element scatter must preserve endpoint/component full-DOF order.");
}
}
}
} catch (const std::out_of_range&) {
return recoveryFailure(
"invalid-recovery-entity",
definition.location,
definition.sourceId.sourceLabelText,
"Every active element requires one twelve-DOF scatter map.");
}
}
return Status::ok();
}
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 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;
}
Result<std::vector<EntityIndex>> resolveLoadTarget(const Domain& domain,
const NodalLoad& load) {
std::vector<const NodeSet*> sets;
for (const auto& set : domain.nodeSets()) {
if (equalName(set.name, load.target)) {
sets.push_back(&set);
}
}
std::vector<EntityIndex> nodes;
std::int64_t sourceLabel = 0;
if (tryPositiveInteger(load.target, sourceLabel)) {
for (std::size_t node = 0U; node < domain.nodes().size(); ++node) {
if (domain.nodes()[node].sourceId.sourceLabel == sourceLabel) {
nodes.push_back(static_cast<EntityIndex>(node));
}
}
}
if (sets.size() > 1U || nodes.size() > 1U ||
(!sets.empty() && !nodes.empty())) {
return recoveryResultFailure<std::vector<EntityIndex>>(
"invalid-node-station-entity",
load.location,
load.target,
"A station-eligibility load target must resolve unambiguously.");
}
if (!sets.empty()) {
std::vector<unsigned char> seen(domain.nodes().size(), 0U);
for (const EntityIndex node : sets.front()->nodeIndices) {
if (node >= domain.nodes().size() || seen[node] != 0U) {
return recoveryResultFailure<std::vector<EntityIndex>>(
"invalid-node-station-entity",
load.location,
load.target,
"A station-eligibility node set must contain unique valid nodes.");
}
seen[node] = 1U;
}
return Result<std::vector<EntityIndex>>::success(
sets.front()->nodeIndices);
}
if (!nodes.empty()) {
return Result<std::vector<EntityIndex>>::success(std::move(nodes));
}
return recoveryResultFailure<std::vector<EntityIndex>>(
"invalid-node-station-entity",
load.location,
load.target,
"A station-eligibility load target must resolve to a node or node set.");
}
std::array<double, 3> cross(const std::array<double, 3>& left,
const std::array<double, 3>& right) {
return {
left[1U] * right[2U] - left[2U] * right[1U],
left[2U] * right[0U] - left[0U] * right[2U],
left[0U] * right[1U] - left[1U] * right[0U]};
}
double dot(const std::array<double, 3>& left,
const std::array<double, 3>& right) {
return left[0U] * right[0U] + left[1U] * right[1U] +
left[2U] * right[2U];
}
double norm(const std::array<double, 3>& value) {
return std::hypot(value[0U], value[1U], value[2U]);
}
std::optional<AxisSet> localAxes(const Domain& domain,
const EulerBeam3DDefinition& element) {
const auto& first = domain.nodes()[element.nodeIndices[0U]].coordinates;
const auto& second = domain.nodes()[element.nodeIndices[1U]].coordinates;
const std::array<double, 3> delta = {
second[0U] - first[0U],
second[1U] - first[1U],
second[2U] - first[2U]};
const double length = norm(delta);
if (!std::isfinite(length) || !(length > 0.0)) {
return std::nullopt;
}
const std::array<double, 3> ex = {
delta[0U] / length, delta[1U] / length, delta[2U] / length};
const auto& guide = domain.sections()[element.sectionIndex].firstAxis;
const double projection = dot(guide, ex);
const std::array<double, 3> eyTrial = {
guide[0U] - projection * ex[0U],
guide[1U] - projection * ex[1U],
guide[2U] - projection * ex[2U]};
const double eyNorm = norm(eyTrial);
if (!std::isfinite(eyNorm) || !(eyNorm > 0.0)) {
return std::nullopt;
}
const std::array<double, 3> ey = {
eyTrial[0U] / eyNorm,
eyTrial[1U] / eyNorm,
eyTrial[2U] / eyNorm};
const std::array<double, 3> ez = cross(ex, ey);
const AxisSet axes = {ex, ey, ez};
for (const auto& axis : axes) {
for (const double component : axis) {
if (!std::isfinite(component)) {
return std::nullopt;
}
}
}
return axes;
}
bool sameAxes(const AxisSet& left, const AxisSet& right) {
for (std::size_t axis = 0U; axis < left.size(); ++axis) {
for (std::size_t component = 0U;
component < left[axis].size();
++component) {
if (std::abs(left[axis][component] - right[axis][component]) >
kAxisTolerance) {
return false;
}
}
}
return true;
}
} // namespace
Status ResultRecovery::recover(const AnalysisModel& model,
const DofManager& dofs,
const SparseMatrix& fullStiffness,
AnalysisState& state) {
const Status inputStatus =
validateRecoveryInputs(model, dofs, fullStiffness, state);
if (!inputStatus.isOk()) {
return inputStatus;
}
Vector internalForce = fullStiffness.multiply(state.displacement());
if (!finite(internalForce)) {
return recoveryFailure(
"nonfinite-recovery-value",
{model.domain().sourcePath(), 0U},
model.domain().sourceContentIdentity(),
"Full stiffness multiplication must produce finite internal force.");
}
Vector residual{dofs.fullDofCount()};
for (std::size_t fullDof = 0U; fullDof < residual.size(); ++fullDof) {
residual[fullDof] =
internalForce[fullDof] - state.externalForce()[fullDof];
if (!std::isfinite(residual[fullDof])) {
return recoveryFailure(
"nonfinite-recovery-value",
{model.domain().sourcePath(), 0U},
std::to_string(fullDof),
"Internal-minus-external residual must remain finite.");
}
}
const double residualNorm = indexedNorm(residual, dofs.freeDofs());
const double internalNorm = indexedNorm(internalForce, dofs.freeDofs());
const double externalNorm =
indexedNorm(state.externalForce(), dofs.freeDofs());
const double denominator = (std::max)(internalNorm, externalNorm);
if (!std::isfinite(residualNorm) || !std::isfinite(denominator)) {
return recoveryFailure(
"nonfinite-recovery-value",
{model.domain().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 normalizedResidual = denominator == 0.0
? (residualNorm == 0.0
? 0.0
: (std::numeric_limits<double>::infinity)())
: residualNorm / denominator;
if (!std::isfinite(normalizedResidual) ||
normalizedResidual > kFreeResidualTolerance) {
return recoveryFailure(
"free-residual-tolerance-failure",
{model.domain().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> endpointRows;
std::vector<GaussResultRow> gaussRows;
std::vector<StressS11Row> stressRows;
endpointRows.reserve(model.activeElements().size() * 2U);
gaussRows.reserve(model.activeElements().size() * 2U);
const Domain& domain = model.domain();
for (const EntityIndex elementIndex : model.activeElements()) {
const auto& definition = domain.elements()[elementIndex];
auto beam = EulerBeam3D::create(
domain.nodes()[definition.nodeIndices[0U]],
domain.nodes()[definition.nodeIndices[1U]],
domain.sections()[definition.sectionIndex],
domain.materials()[definition.materialIndex]);
if (!beam.hasValue()) {
return beam.status();
}
Vector elementDisplacement{kElementDofCount};
const auto& scatter = dofs.elementScatter(elementIndex);
for (std::size_t localDof = 0U;
localDof < kElementDofCount;
++localDof) {
elementDisplacement[localDof] =
state.displacement()[scatter[localDof]];
}
const BeamRecovery recovered =
beam.value().recover(elementDisplacement);
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
if (!finite(recovered.equilibriumEndActions[endpoint]) ||
!finite(recovered.endpointSectionResultants[endpoint])) {
return recoveryFailure(
"nonfinite-recovery-value",
definition.location,
definition.sourceId.sourceLabelText,
"Endpoint recovery values must be finite.");
}
endpointRows.push_back({
elementIndex,
static_cast<int>(endpoint),
domain.nodes()[definition.nodeIndices[endpoint]].sourceId,
recovered.equilibriumEndActions[endpoint],
recovered.endpointSectionResultants[endpoint]});
}
for (std::size_t gauss = 0U; gauss < 2U; ++gauss) {
if (!finite(recovered.gaussGeneralizedStrains[gauss]) ||
!finite(recovered.gaussGeneralizedResultants[gauss])) {
return recoveryFailure(
"nonfinite-recovery-value",
definition.location,
definition.sourceId.sourceLabelText,
"Gauss recovery values must be finite.");
}
gaussRows.push_back({
elementIndex,
static_cast<int>(gauss + 1U),
recovered.gaussGeneralizedStrains[gauss],
recovered.gaussGeneralizedResultants[gauss]});
}
for (const auto& point : recovered.stressPoints) {
if ((point.gaussPoint != 1 && point.gaussPoint != 2) ||
!std::isfinite(point.x1) || !std::isfinite(point.x2) ||
!std::isfinite(point.s11)) {
return recoveryFailure(
"nonfinite-recovery-value",
definition.location,
definition.sourceId.sourceLabelText,
"Stress recovery identity and values must be finite and ordered.");
}
stressRows.push_back({
elementIndex,
point.gaussPoint,
point.sectionPoint,
point.x1,
point.x2,
point.s11,
point.source});
}
}
// Commit only after all validation and element recovery succeeds so a
// failed recovery cannot leave a partially updated AnalysisState.
state.internalForce() = std::move(internalForce);
state.residual() = std::move(residual);
state.reaction() = std::move(reaction);
state.endpointResults() = std::move(endpointRows);
state.gaussResults() = std::move(gaussRows);
state.stressResults() = std::move(stressRows);
return Status::ok();
}
Result<std::vector<NodeStationResultRow>>
ResultRecovery::normalizeSectionResultantsToNodeStations(
const AnalysisModel& model,
const std::vector<EndpointResultRow>& endpointRows,
const std::array<double, 4>& componentTolerances) {
const Domain& domain = model.domain();
for (const double tolerance : componentTolerances) {
if (!std::isfinite(tolerance) || tolerance < 0.0) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-tolerance",
{domain.sourcePath(), 0U},
"component-tolerances",
"Node-station component tolerances must be finite and nonnegative.");
}
}
if (model.activeElements().size() >
(std::numeric_limits<std::size_t>::max)() / 2U ||
endpointRows.size() != model.activeElements().size() * 2U) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-shape",
{domain.sourcePath(), 0U},
std::to_string(endpointRows.size()),
"Endpoint rows must contain exactly two rows per active element.");
}
std::vector<std::vector<const EndpointResultRow*>> rowsByNode(
domain.nodes().size());
for (std::size_t order = 0U;
order < model.activeElements().size();
++order) {
const EntityIndex elementIndex = model.activeElements()[order];
if (elementIndex >= domain.elements().size()) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-entity",
{domain.sourcePath(), 0U},
std::to_string(elementIndex),
"Every active station element must be a valid stable entity.");
}
const auto& definition = domain.elements()[elementIndex];
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
const auto& row = endpointRows[order * 2U + endpoint];
const EntityIndex nodeIndex = definition.nodeIndices[endpoint];
if (nodeIndex >= domain.nodes().size() ||
row.element != elementIndex ||
row.endpoint != static_cast<int>(endpoint) ||
!sameSourceIdentity(row.node, domain.nodes()[nodeIndex].sourceId)) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-entity",
definition.location,
definition.sourceId.sourceLabelText,
"Endpoint rows must preserve active element, endpoint, and source-node order.");
}
if (!finite(row.sectionResultant)) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"nonfinite-node-station-value",
definition.location,
definition.sourceId.sourceLabelText,
"Node-station section resultants must be finite.");
}
rowsByNode[nodeIndex].push_back(&row);
}
}
std::vector<unsigned char> loadedNodes(domain.nodes().size(), 0U);
if (model.activeLoads().size() != model.step().loads.size()) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-entity",
model.step().location,
model.step().name,
"The active load view must preserve every sole-step load.");
}
for (std::size_t order = 0U; order < model.activeLoads().size(); ++order) {
const EntityIndex loadIndex = model.activeLoads()[order];
if (loadIndex != order || loadIndex >= model.step().loads.size()) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"invalid-node-station-entity",
model.step().location,
std::to_string(loadIndex),
"Active loads must remain in stable source order.");
}
const auto& load = model.step().loads[loadIndex];
if (!std::isfinite(load.magnitude)) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"nonfinite-node-station-value",
load.location,
load.target,
"Station eligibility requires finite concentrated loads.");
}
auto targets = resolveLoadTarget(domain, load);
if (!targets.hasValue()) {
return Result<std::vector<NodeStationResultRow>>::failure(
targets.status());
}
if (load.magnitude != 0.0) {
for (const EntityIndex node : targets.value()) {
loadedNodes[node] = 1U;
}
}
}
std::vector<NodeStationResultRow> stations;
stations.reserve(domain.nodes().size());
for (std::size_t nodeIndex = 0U;
nodeIndex < rowsByNode.size();
++nodeIndex) {
const auto& incident = rowsByNode[nodeIndex];
if (incident.empty()) {
continue;
}
if (incident.size() == 1U) {
stations.push_back({
domain.nodes()[nodeIndex].sourceId,
incident.front()->element,
incident.front()->sectionResultant});
continue;
}
if (incident.size() != 2U || loadedNodes[nodeIndex] != 0U) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"ineligible-node-station",
domain.nodes()[nodeIndex].location,
domain.nodes()[nodeIndex].sourceId.sourceLabelText,
"Interior station collapse requires exactly two unloaded endpoints.");
}
const auto& firstElement = domain.elements()[incident[0U]->element];
const auto& secondElement = domain.elements()[incident[1U]->element];
const bool chainOrientation =
incident[0U]->endpoint != incident[1U]->endpoint &&
((incident[0U]->endpoint == 1 && incident[1U]->endpoint == 0) ||
(incident[0U]->endpoint == 0 && incident[1U]->endpoint == 1));
const auto firstAxes = localAxes(domain, firstElement);
const auto secondAxes = localAxes(domain, secondElement);
if (!chainOrientation ||
firstElement.sectionIndex != secondElement.sectionIndex ||
!firstAxes.has_value() || !secondAxes.has_value() ||
!sameAxes(*firstAxes, *secondAxes)) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"ineligible-node-station",
domain.nodes()[nodeIndex].location,
domain.nodes()[nodeIndex].sourceId.sourceLabelText,
"Interior station endpoints require one consistent section and local-axis chain.");
}
// Endpoint sectionResultant rows already use the positive-local-x cut
// convention. Once common orientation is proven, no outward-action
// endpoint sign is applied and the values are directly comparable.
for (std::size_t component = 0U;
component < componentTolerances.size();
++component) {
const double difference = std::abs(
incident[0U]->sectionResultant[component] -
incident[1U]->sectionResultant[component]);
if (!std::isfinite(difference)) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"nonfinite-node-station-value",
domain.nodes()[nodeIndex].location,
domain.nodes()[nodeIndex].sourceId.sourceLabelText,
"Endpoint comparison must produce a finite difference.");
}
if (difference > componentTolerances[component]) {
return recoveryResultFailure<std::vector<NodeStationResultRow>>(
"node-station-tolerance-failure",
domain.nodes()[nodeIndex].location,
domain.nodes()[nodeIndex].sourceId.sourceLabelText,
"Interior endpoint resultants disagree beyond component tolerance.");
}
}
const EndpointResultRow* representative =
incident[0U]->element < incident[1U]->element
? incident[0U]
: incident[1U];
stations.push_back({
domain.nodes()[nodeIndex].sourceId,
representative->element,
representative->sectionResultant});
}
return Result<std::vector<NodeStationResultRow>>::success(
std::move(stations));
}
} // namespace fesa