feat(linear-static-mitc4-shell): step 10 - shell-result-recovery

This commit is contained in:
KOKO\Mimi
2026-08-12 21:24:06 +09:00
parent d759cd7ab8
commit cef42dfa44
3 changed files with 882 additions and 16 deletions
+362 -15
View File
@@ -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();
}