feat(linear-static-3d-euler-beam): step 22 - result-recovery
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user