feat(linear-static-mitc4-shell): step 10 - shell-result-recovery
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include "fesa/results/result_recovery.hpp"
|
||||
|
||||
#include "fesa/elements/euler_beam_3d.hpp"
|
||||
#include "fesa/elements/mitc4_shell.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -21,7 +22,10 @@ namespace {
|
||||
|
||||
constexpr std::size_t kDofsPerNode = 6U;
|
||||
constexpr std::size_t kElementDofCount = 12U;
|
||||
constexpr std::size_t kShellElementDofCount = 24U;
|
||||
constexpr std::size_t kShellLocationCount = 4U;
|
||||
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>;
|
||||
@@ -56,13 +60,8 @@ bool sameSourceIdentity(const SourceEntityId& left,
|
||||
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) {
|
||||
template<std::size_t Size>
|
||||
bool finite(const std::array<double, Size>& values) {
|
||||
return std::all_of(
|
||||
values.begin(), values.end(),
|
||||
[](const double value) { return std::isfinite(value); });
|
||||
@@ -252,6 +251,74 @@ Status validateRecoveryInputs(const AnalysisModel& model,
|
||||
"Every active element requires one twelve-DOF scatter map.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!model.activeElements().empty() && !domain.shellElements().empty()) {
|
||||
return recoveryFailure(
|
||||
"unsupported-mixed-element-model",
|
||||
{domain.sourcePath(), 0U},
|
||||
"B33:FESA-MITC4",
|
||||
"Result recovery does not support mixed beam and shell models.");
|
||||
}
|
||||
if (domain.shellElements().size() >
|
||||
static_cast<std::size_t>(
|
||||
(std::numeric_limits<EntityIndex>::max)())) {
|
||||
return recoveryFailure(
|
||||
"invalid-recovery-dimensions",
|
||||
{domain.sourcePath(), 0U},
|
||||
domain.sourceContentIdentity(),
|
||||
"The shell element count cannot be represented by stable element identities.");
|
||||
}
|
||||
for (std::size_t elementOrder = 0U;
|
||||
elementOrder < domain.shellElements().size();
|
||||
++elementOrder) {
|
||||
const auto& definition = domain.shellElements()[elementOrder];
|
||||
if (definition.materialIndex >= domain.materials().size() ||
|
||||
definition.sectionIndex >= domain.shellSections().size()) {
|
||||
return recoveryFailure(
|
||||
"invalid-recovery-entity",
|
||||
definition.location,
|
||||
definition.sourceId.sourceLabelText,
|
||||
"Active shell material and section references must resolve before recovery.");
|
||||
}
|
||||
try {
|
||||
const auto& scatter = dofs.shellElementScatter(
|
||||
static_cast<EntityIndex>(elementOrder));
|
||||
for (std::size_t nodePosition = 0U;
|
||||
nodePosition < definition.nodeIndices.size();
|
||||
++nodePosition) {
|
||||
const EntityIndex node = definition.nodeIndices[nodePosition];
|
||||
if (node >= domain.nodes().size()) {
|
||||
return recoveryFailure(
|
||||
"invalid-recovery-entity",
|
||||
definition.location,
|
||||
definition.sourceId.sourceLabelText,
|
||||
"Active shell node references must resolve before recovery.");
|
||||
}
|
||||
for (std::size_t component = 0U;
|
||||
component < kDofsPerNode;
|
||||
++component) {
|
||||
const std::size_t local =
|
||||
nodePosition * kDofsPerNode + component;
|
||||
const std::size_t expected =
|
||||
static_cast<std::size_t>(node) * kDofsPerNode +
|
||||
component;
|
||||
if (scatter[local] != expected || expected >= fullCount) {
|
||||
return recoveryFailure(
|
||||
"invalid-recovery-order",
|
||||
definition.location,
|
||||
definition.sourceId.sourceLabelText,
|
||||
"Shell scatter must preserve node/component full-DOF order.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::out_of_range&) {
|
||||
return recoveryFailure(
|
||||
"invalid-recovery-entity",
|
||||
definition.location,
|
||||
definition.sourceId.sourceLabelText,
|
||||
"Every active shell requires one twenty-four-DOF scatter map.");
|
||||
}
|
||||
}
|
||||
return Status::ok();
|
||||
}
|
||||
|
||||
@@ -346,6 +413,144 @@ double norm(const std::array<double, 3>& value) {
|
||||
return std::hypot(value[0U], value[1U], value[2U]);
|
||||
}
|
||||
|
||||
bool accumulateVectorAndScale(
|
||||
std::array<double, 3>& total,
|
||||
double& scale,
|
||||
const std::array<double, 3>& contribution) {
|
||||
const double magnitude = norm(contribution);
|
||||
const double accumulatedScale = scale + magnitude;
|
||||
if (!finite(contribution) || !std::isfinite(magnitude) ||
|
||||
!std::isfinite(accumulatedScale)) {
|
||||
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 = accumulatedScale;
|
||||
return true;
|
||||
}
|
||||
|
||||
double normalizedBalance(
|
||||
const std::array<double, 3>& balance,
|
||||
const double scale) {
|
||||
const double balanceNorm = norm(balance);
|
||||
if (!std::isfinite(balanceNorm) || !std::isfinite(scale)) {
|
||||
return (std::numeric_limits<double>::infinity)();
|
||||
}
|
||||
if (scale == 0.0) {
|
||||
return balanceNorm == 0.0
|
||||
? 0.0
|
||||
: (std::numeric_limits<double>::infinity)();
|
||||
}
|
||||
return balanceNorm / scale;
|
||||
}
|
||||
|
||||
Status populateShellGlobalEvidence(
|
||||
const Domain& domain,
|
||||
const DofManager& dofs,
|
||||
const Vector& externalForce,
|
||||
const Vector& residual,
|
||||
const double normalizedResidual,
|
||||
ShellStateCandidate& candidate) {
|
||||
std::vector<unsigned char> constrained(dofs.fullDofCount(), 0U);
|
||||
for (const std::size_t fullDof : dofs.constrainedDofs()) {
|
||||
constrained[fullDof] = 1U;
|
||||
}
|
||||
|
||||
std::array<double, 3> appliedForce{};
|
||||
std::array<double, 3> reactionForce{};
|
||||
std::array<double, 3> appliedMoment{};
|
||||
std::array<double, 3> reactionMoment{};
|
||||
double appliedForceScale = 0.0;
|
||||
double reactionForceScale = 0.0;
|
||||
double appliedMomentScale = 0.0;
|
||||
double reactionMomentScale = 0.0;
|
||||
for (std::size_t node = 0U; node < domain.nodes().size(); ++node) {
|
||||
const std::size_t offset = node * kDofsPerNode;
|
||||
std::array<double, 3> nodalAppliedForce{};
|
||||
std::array<double, 3> nodalReactionForce{};
|
||||
std::array<double, 3> nodalAppliedMoment{};
|
||||
std::array<double, 3> nodalReactionMoment{};
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
nodalAppliedForce[component] = externalForce[offset + component];
|
||||
nodalAppliedMoment[component] =
|
||||
externalForce[offset + 3U + component];
|
||||
if (constrained[offset + component] != 0U) {
|
||||
nodalReactionForce[component] = residual[offset + component];
|
||||
}
|
||||
if (constrained[offset + 3U + component] != 0U) {
|
||||
nodalReactionMoment[component] =
|
||||
residual[offset + 3U + component];
|
||||
}
|
||||
}
|
||||
|
||||
const auto appliedForceMoment =
|
||||
cross(domain.nodes()[node].coordinates, nodalAppliedForce);
|
||||
const auto reactionForceMoment =
|
||||
cross(domain.nodes()[node].coordinates, nodalReactionForce);
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
nodalAppliedMoment[component] += appliedForceMoment[component];
|
||||
nodalReactionMoment[component] += reactionForceMoment[component];
|
||||
}
|
||||
if (!accumulateVectorAndScale(
|
||||
appliedForce, appliedForceScale, nodalAppliedForce) ||
|
||||
!accumulateVectorAndScale(
|
||||
reactionForce, reactionForceScale, nodalReactionForce) ||
|
||||
!accumulateVectorAndScale(
|
||||
appliedMoment, appliedMomentScale, nodalAppliedMoment) ||
|
||||
!accumulateVectorAndScale(
|
||||
reactionMoment, reactionMomentScale, nodalReactionMoment)) {
|
||||
return recoveryFailure(
|
||||
"nonfinite-recovery-value",
|
||||
domain.nodes()[node].location,
|
||||
domain.nodes()[node].sourceId.sourceLabelText,
|
||||
"Global force and moment evidence must remain finite in source-node order.");
|
||||
}
|
||||
}
|
||||
|
||||
std::array<double, 3> forceBalance{};
|
||||
std::array<double, 3> momentBalance{};
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
forceBalance[component] =
|
||||
appliedForce[component] + reactionForce[component];
|
||||
momentBalance[component] =
|
||||
appliedMoment[component] + reactionMoment[component];
|
||||
candidate.equilibrium[component] = forceBalance[component];
|
||||
candidate.equilibrium[3U + component] = momentBalance[component];
|
||||
}
|
||||
const double forceMetric = normalizedBalance(
|
||||
forceBalance,
|
||||
(std::max)(appliedForceScale, reactionForceScale));
|
||||
const double momentMetric = normalizedBalance(
|
||||
momentBalance,
|
||||
(std::max)(appliedMomentScale, reactionMomentScale));
|
||||
candidate.verificationMetrics = {
|
||||
normalizedResidual, forceMetric, momentMetric};
|
||||
if (!finite(candidate.equilibrium) ||
|
||||
!finite(candidate.verificationMetrics)) {
|
||||
return recoveryFailure(
|
||||
"nonfinite-recovery-value",
|
||||
{domain.sourcePath(), 0U},
|
||||
"global-equilibrium",
|
||||
"Global equilibrium values and their physical normalization scales must be finite.");
|
||||
}
|
||||
if (forceMetric > kGlobalEquilibriumTolerance ||
|
||||
momentMetric > 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 EulerBeam3DDefinition& element) {
|
||||
const auto& first = domain.nodes()[element.nodeIndices[0U]].coordinates;
|
||||
@@ -545,14 +750,156 @@ Status ResultRecovery::recover(const AnalysisModel& model,
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
ShellStateCandidate shellCandidate{};
|
||||
std::vector<EntityIndex> expectedShellElements;
|
||||
if (!domain.shellElements().empty()) {
|
||||
if (domain.shellElements().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() /
|
||||
kShellLocationCount) {
|
||||
return recoveryFailure(
|
||||
"invalid-recovery-dimensions",
|
||||
{domain.sourcePath(), 0U},
|
||||
domain.sourceContentIdentity(),
|
||||
"The shell result-row inventory exceeds the addressable range.");
|
||||
}
|
||||
std::vector<std::optional<std::array<double, 3>>> directorsByNode(
|
||||
domain.nodes().size());
|
||||
for (const auto& frame : domain.shellNodeInitialFrames()) {
|
||||
if (frame.nodeIndex >= directorsByNode.size() ||
|
||||
directorsByNode[frame.nodeIndex].has_value()) {
|
||||
return recoveryFailure(
|
||||
"invalid-recovery-entity",
|
||||
{domain.sourcePath(), 0U},
|
||||
std::to_string(frame.nodeIndex),
|
||||
"Shell initial directors must map uniquely to model nodes.");
|
||||
}
|
||||
directorsByNode[frame.nodeIndex] = frame.director;
|
||||
}
|
||||
|
||||
shellCandidate.rows.reserve(
|
||||
domain.shellElements().size() * kShellLocationCount);
|
||||
expectedShellElements.reserve(domain.shellElements().size());
|
||||
constexpr std::array<ShellMidsurfaceLocation, kShellLocationCount>
|
||||
locations{
|
||||
ShellMidsurfaceLocation::gp1,
|
||||
ShellMidsurfaceLocation::gp2,
|
||||
ShellMidsurfaceLocation::gp3,
|
||||
ShellMidsurfaceLocation::gp4};
|
||||
constexpr std::array<ShellSectionPosition, 3> positions{
|
||||
ShellSectionPosition::bottom,
|
||||
ShellSectionPosition::middle,
|
||||
ShellSectionPosition::top};
|
||||
constexpr std::array<double, 3> zeta{-1.0, 0.0, 1.0};
|
||||
for (std::size_t elementOrder = 0U;
|
||||
elementOrder < domain.shellElements().size();
|
||||
++elementOrder) {
|
||||
const EntityIndex elementIndex =
|
||||
static_cast<EntityIndex>(elementOrder);
|
||||
const auto& definition = domain.shellElements()[elementOrder];
|
||||
std::array<const Node*, 4> nodes{};
|
||||
std::array<std::array<double, 3>, 4> directors{};
|
||||
for (std::size_t nodePosition = 0U;
|
||||
nodePosition < definition.nodeIndices.size();
|
||||
++nodePosition) {
|
||||
const EntityIndex node = definition.nodeIndices[nodePosition];
|
||||
if (!directorsByNode[node].has_value()) {
|
||||
return recoveryFailure(
|
||||
"invalid-recovery-entity",
|
||||
definition.location,
|
||||
definition.sourceId.sourceLabelText,
|
||||
"Shell recovery requires one initial director per element node.");
|
||||
}
|
||||
nodes[nodePosition] = &domain.nodes()[node];
|
||||
directors[nodePosition] = *directorsByNode[node];
|
||||
}
|
||||
|
||||
auto shell = Mitc4Shell::create(
|
||||
nodes,
|
||||
directors,
|
||||
domain.shellSections()[definition.sectionIndex],
|
||||
domain.materials()[definition.materialIndex]);
|
||||
if (!shell.hasValue()) {
|
||||
return shell.status();
|
||||
}
|
||||
Vector elementDisplacement{kShellElementDofCount};
|
||||
const auto& scatter = dofs.shellElementScatter(elementIndex);
|
||||
for (std::size_t localDof = 0U;
|
||||
localDof < kShellElementDofCount;
|
||||
++localDof) {
|
||||
elementDisplacement[localDof] =
|
||||
state.displacement()[scatter[localDof]];
|
||||
}
|
||||
auto recovered = shell.value().recoverPhysical(
|
||||
elementDisplacement);
|
||||
if (!recovered.hasValue()) {
|
||||
return recovered.status();
|
||||
}
|
||||
const double accumulatedEnergy =
|
||||
shellCandidate.physicalStrainEnergy +
|
||||
recovered.value().strainEnergy;
|
||||
if (!std::isfinite(accumulatedEnergy)) {
|
||||
return recoveryFailure(
|
||||
"nonfinite-recovery-value",
|
||||
definition.location,
|
||||
definition.sourceId.sourceLabelText,
|
||||
"Source-order physical shell energy reduction must remain finite.");
|
||||
}
|
||||
shellCandidate.physicalStrainEnergy = accumulatedEnergy;
|
||||
expectedShellElements.push_back(elementIndex);
|
||||
|
||||
for (std::size_t point = 0U;
|
||||
point < recovered.value().points.size();
|
||||
++point) {
|
||||
const auto& physicalPoint = recovered.value().points[point];
|
||||
ShellResultRow row{};
|
||||
row.element = elementIndex;
|
||||
row.location = locations[point];
|
||||
row.naturalCoordinates = physicalPoint.naturalCoordinates;
|
||||
row.localFrame = {
|
||||
physicalPoint.localFrame.e1,
|
||||
physicalPoint.localFrame.e2,
|
||||
physicalPoint.localFrame.e3};
|
||||
row.generalizedStrain = physicalPoint.generalizedStrain;
|
||||
row.sectionResultant = physicalPoint.sectionResultant;
|
||||
for (std::size_t position = 0U;
|
||||
position < positions.size();
|
||||
++position) {
|
||||
row.stress[position] = {
|
||||
positions[position],
|
||||
zeta[position],
|
||||
physicalPoint.inPlaneStress[position]};
|
||||
}
|
||||
shellCandidate.rows.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
const Status evidenceStatus = populateShellGlobalEvidence(
|
||||
domain,
|
||||
dofs,
|
||||
state.externalForce(),
|
||||
residual,
|
||||
normalizedResidual,
|
||||
shellCandidate);
|
||||
if (!evidenceStatus.isOk()) {
|
||||
return evidenceStatus;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 candidateState = state;
|
||||
candidateState.internalForce() = std::move(internalForce);
|
||||
candidateState.residual() = std::move(residual);
|
||||
candidateState.reaction() = std::move(reaction);
|
||||
candidateState.endpointResults() = std::move(endpointRows);
|
||||
candidateState.gaussResults() = std::move(gaussRows);
|
||||
candidateState.stressResults() = std::move(stressRows);
|
||||
const Status shellCommitStatus = candidateState.commitShellResults(
|
||||
expectedShellElements, std::move(shellCandidate));
|
||||
if (!shellCommitStatus.isOk()) {
|
||||
return shellCommitStatus;
|
||||
}
|
||||
state = std::move(candidateState);
|
||||
return Status::ok();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user