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
+519
View File
@@ -2,6 +2,7 @@
#include "fesa/analysis/analysis_model.hpp"
#include "fesa/analysis/analysis_state.hpp"
#include "fesa/assembly/load_assembler.hpp"
#include "fesa/assembly/parallel_for.hpp"
#include "fesa/assembly/sparse_assembler.hpp"
#include "fesa/fem/dof_manager.hpp"
@@ -34,6 +35,13 @@ struct RecoveryFixture {
std::unique_ptr<fesa::SparseMatrix> stiffness;
};
struct ShellRecoveryFixture {
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::ModelDefinition makeDefinition(
const bool twoElements = false,
std::vector<std::array<double, 2>> sectionPoints = {},
@@ -144,6 +152,148 @@ RecoveryFixture makeFixture(
std::move(stiffness)};
}
fesa::ModelDefinition makeShellDefinition(
const bool twoElements = false,
const bool constrainAll = false,
std::vector<fesa::NodalLoad> loads = {}) {
const std::filesystem::path source{"models/shell-result-recovery.inp"};
fesa::ModelDefinition definition{};
definition.sourcePath = source;
definition.sourceContentIdentity = "fnv1a64:fedcba9876543210";
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"}, {3.0, -1.0, 0.0}, {source, 12U}},
{{"Shell-1", 4, "4"}, {-1.0, 1.0, 0.0}, {source, 13U}},
{{"Shell-1", 5, "5"}, {1.0, 1.0, 0.0}, {source, 14U}},
{{"Shell-1", 6, "6"}, {3.0, 1.0, 0.0}, {source, 15U}}};
if (!twoElements) {
definition.nodes.erase(
definition.nodes.begin() + 2,
definition.nodes.begin() + 3);
definition.nodes.erase(definition.nodes.begin() + 4);
}
definition.materials = {
{"Material", 120.0, 0.25, {source, 20U}}};
definition.shellSections = {
{"ShellSection", 2.0, 0U, {source, 30U}}};
definition.shellElements = {{
{"Shell-1", 10, "10"},
fesa::ShellSourceElementType::s4,
{0U, 1U, twoElements ? 4U : 3U, twoElements ? 3U : 2U},
0U,
0U,
{source, 40U}}};
if (twoElements) {
definition.shellElements.push_back({
{"Shell-1", 20, "20"},
fesa::ShellSourceElementType::s4r,
{1U, 2U, 5U, 4U},
0U,
0U,
{source, 41U}});
}
for (std::size_t node = 0U; node < definition.nodes.size(); ++node) {
definition.shellNodeInitialFrames.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}});
}
if (constrainAll) {
std::vector<fesa::EntityIndex> allNodes;
allNodes.reserve(definition.nodes.size());
for (std::size_t node = 0U; node < definition.nodes.size(); ++node) {
allNodes.push_back(static_cast<fesa::EntityIndex>(node));
}
definition.nodeSets.push_back(
{"All", {}, std::move(allNodes), {source, 50U}});
}
definition.steps = {{
"Step-1",
constrainAll
? std::vector<fesa::BoundaryCondition>{
{"All", 1, 6, 0.0, {source, 60U}}}
: std::vector<fesa::BoundaryCondition>{},
std::move(loads),
0.1,
1.0,
0.01,
1.0,
{source, 59U}}};
return definition;
}
ShellRecoveryFixture makeShellFixture(fesa::ModelDefinition definition) {
auto domainResult = fesa::Domain::create(std::move(definition));
if (!domainResult.hasValue()) {
throw std::runtime_error{
"Shell recovery fixture Domain construction failed."};
}
auto domain = std::make_unique<fesa::Domain>(
std::move(domainResult.value()));
auto modelResult = fesa::AnalysisModel::create(*domain);
if (!modelResult.hasValue()) {
throw std::runtime_error{
"Shell recovery fixture AnalysisModel construction failed."};
}
auto model = std::make_unique<fesa::AnalysisModel>(
std::move(modelResult.value()));
auto dofsResult = fesa::DofManager::create(*model);
if (!dofsResult.hasValue()) {
throw std::runtime_error{
"Shell recovery fixture DofManager construction failed."};
}
auto dofs = std::make_unique<fesa::DofManager>(
std::move(dofsResult.value()));
fesa::SerialParallelFor serial;
auto stiffnessResult = fesa::SparseAssembler::assembleStiffness(
*model, *dofs, serial);
if (!stiffnessResult.hasValue()) {
throw std::runtime_error{
"Shell recovery fixture stiffness assembly failed."};
}
auto stiffness = std::make_unique<fesa::SparseMatrix>(
std::move(stiffnessResult.value()));
return {
std::move(domain),
std::move(model),
std::move(dofs),
std::move(stiffness)};
}
fesa::AnalysisState makeShellPhysicalState(
const ShellRecoveryFixture& 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;
}
fesa::AnalysisState makeAxialEquilibriumState(const RecoveryFixture& fixture) {
auto state = fesa::AnalysisState::create(
*fixture.dofs, {"Step-1", 0U});
@@ -217,6 +367,11 @@ std::vector<fesa::EndpointResultRow> makeStationRows(
TEST(ResultRecovery, ComputesResidualReactionForNonzeroPrescription) {
const auto fixture = makeFixture();
auto state = makeAxialEquilibriumState(fixture);
fesa::ShellStateCandidate staleShellEvidence{};
staleShellEvidence.physicalStrainEnergy = 123.0;
staleShellEvidence.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
staleShellEvidence.verificationMetrics = {1.0e-11, 2.0e-11, 3.0e-11};
ASSERT_TRUE(state.commitShellResults({}, staleShellEvidence).isOk());
const fesa::Status status = fesa::ResultRecovery::recover(
*fixture.model, *fixture.dofs, *fixture.stiffness, state);
@@ -230,6 +385,14 @@ TEST(ResultRecovery, ComputesResidualReactionForNonzeroPrescription) {
for (const std::size_t fullDof : fixture.dofs->freeDofs()) {
EXPECT_DOUBLE_EQ(state.reaction()[fullDof], 0.0);
}
EXPECT_TRUE(state.shellResults().empty());
EXPECT_DOUBLE_EQ(state.physicalStrainEnergy(), 0.0);
EXPECT_EQ(
state.equilibrium(),
(std::array<double, 6>{0.0, 0.0, 0.0, 0.0, 0.0, 0.0}));
EXPECT_EQ(
state.verificationMetrics(),
(std::array<double, 3>{0.0, 0.0, 0.0}));
}
TEST(ResultRecovery, EnforcesNormalizedFreeResidual) {
@@ -469,3 +632,359 @@ TEST(ResultRecovery, RequiresInteriorEndpointConsistencyWithoutAveraging) {
ASSERT_FALSE(jumped.hasValue());
expectStatusCode(jumped.status(), "ineligible-node-station");
}
// MITC4-REC-001
TEST(ResultRecovery, RecoversShellRowsInStableElementAndGpOrder) {
const auto fixture = makeShellFixture(makeShellDefinition(true));
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];
state.displacement()[node * 6U] = 0.1 * x + 0.1 * y;
state.displacement()[node * 6U + 1U] = -0.05 * y + 0.1 * x;
}
state.externalForce() =
fixture.stiffness->multiply(state.displacement());
const auto status = fesa::ResultRecovery::recover(
*fixture.model, *fixture.dofs, *fixture.stiffness, state);
ASSERT_TRUE(status.isOk());
ASSERT_EQ(state.shellResults().size(), 8U);
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<fesa::ShellMidsurfaceLocation, 4> locations{
fesa::ShellMidsurfaceLocation::gp1,
fesa::ShellMidsurfaceLocation::gp2,
fesa::ShellMidsurfaceLocation::gp3,
fesa::ShellMidsurfaceLocation::gp4};
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}};
constexpr std::array<double, 8> expectedStrain{
0.1, -0.05, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0};
constexpr std::array<double, 8> expectedResultant{
22.4, -6.4, 19.2, 0.0, 0.0, 0.0, 0.0, 0.0};
const std::array<std::array<double, 3>, 3> expectedFrame{{
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0}}};
for (std::size_t element = 0U; element < 2U; ++element) {
for (std::size_t point = 0U; point < locations.size(); ++point) {
const auto& row = state.shellResults()[element * 4U + point];
EXPECT_EQ(row.element, element);
EXPECT_EQ(row.location, locations[point]);
EXPECT_EQ(row.naturalCoordinates, coordinates[point]);
EXPECT_EQ(row.localFrame, expectedFrame);
for (std::size_t component = 0U;
component < expectedStrain.size();
++component) {
EXPECT_NEAR(
row.generalizedStrain[component],
expectedStrain[component],
1.0e-12);
EXPECT_NEAR(
row.sectionResultant[component],
expectedResultant[component],
1.0e-12);
}
}
}
}
// MITC4-REC-002
TEST(ResultRecovery, RecoversDirectBottomMiddleTopShellStress) {
const auto fixture = makeShellFixture(makeShellDefinition());
auto state = makeShellPhysicalState(fixture);
ASSERT_TRUE(fesa::ResultRecovery::recover(
*fixture.model,
*fixture.dofs,
*fixture.stiffness,
state)
.isOk());
constexpr std::array<fesa::ShellSectionPosition, 3> positions{
fesa::ShellSectionPosition::bottom,
fesa::ShellSectionPosition::middle,
fesa::ShellSectionPosition::top};
constexpr std::array<double, 3> zeta{-1.0, 0.0, 1.0};
constexpr std::array<std::array<double, 3>, 3> expectedStress{{
{-22.4, 6.4, -2.4},
{11.2, -3.2, 9.6},
{44.8, -12.8, 21.6}}};
ASSERT_EQ(state.shellResults().size(), 4U);
for (const auto& row : state.shellResults()) {
for (std::size_t position = 0U;
position < positions.size();
++position) {
EXPECT_EQ(row.stress[position].position, positions[position]);
EXPECT_DOUBLE_EQ(row.stress[position].zeta, zeta[position]);
for (std::size_t component = 0U;
component < expectedStress[position].size();
++component) {
EXPECT_NEAR(
row.stress[position].components[component],
expectedStress[position][component],
1.0e-12);
}
}
}
}
// MITC4-REC-003
TEST(ResultRecovery, SumsOnlyPhysicalShellEnergyInSourceOrder) {
const auto fixture = makeShellFixture(makeShellDefinition());
auto state = makeShellPhysicalState(fixture);
constexpr std::array<double, 4> drill{1.0, -1.0, 1.0, -1.0};
for (std::size_t node = 0U; node < drill.size(); ++node) {
state.displacement()[node * 6U + 5U] = drill[node];
}
state.externalForce() =
fixture.stiffness->multiply(state.displacement());
const double stabilizedEnergy = 0.5 * state.displacement().dot(
fixture.stiffness->multiply(state.displacement()));
ASSERT_TRUE(fesa::ResultRecovery::recover(
*fixture.model,
*fixture.dofs,
*fixture.stiffness,
state)
.isOk());
EXPECT_NEAR(state.physicalStrainEnergy(), 72.16, 1.0e-12);
EXPECT_GT(stabilizedEnergy, state.physicalStrainEnergy());
}
// MITC4-REC-004
TEST(ResultRecovery, KeepsFullResidualAndComputesGlobalShellEquilibrium) {
const std::filesystem::path source{"models/shell-result-recovery.inp"};
const std::vector<fesa::NodalLoad> loads{
{"1", 1, 5.0, {source, 70U}},
{"5", 1, -5.0, {source, 71U}},
{"1", 4, 2.0, {source, 72U}},
{"5", 4, -2.0, {source, 73U}}};
const auto fixture = makeShellFixture(
makeShellDefinition(false, true, loads));
auto state = fesa::AnalysisState::create(
*fixture.dofs, {"Step-1", 0U});
auto fullLoad = fesa::LoadAssembler::assembleFullNodalLoad(
*fixture.model, *fixture.dofs);
ASSERT_TRUE(fullLoad.hasValue());
state.externalForce() = std::move(fullLoad.value());
ASSERT_TRUE(fesa::ResultRecovery::recover(
*fixture.model,
*fixture.dofs,
*fixture.stiffness,
state)
.isOk());
ASSERT_EQ(state.shellResults().size(), 4U);
for (std::size_t fullDof = 0U;
fullDof < fixture.dofs->fullDofCount();
++fullDof) {
EXPECT_DOUBLE_EQ(state.internalForce()[fullDof], 0.0);
EXPECT_DOUBLE_EQ(
state.residual()[fullDof], -state.externalForce()[fullDof]);
EXPECT_DOUBLE_EQ(state.reaction()[fullDof], state.residual()[fullDof]);
}
EXPECT_EQ(
state.equilibrium(),
(std::array<double, 6>{0.0, 0.0, 0.0, 0.0, 0.0, 0.0}));
EXPECT_EQ(
state.verificationMetrics(),
(std::array<double, 3>{0.0, 0.0, 0.0}));
const auto freeFixture = makeShellFixture(makeShellDefinition());
auto perturbed = makeShellPhysicalState(freeFixture);
perturbed.externalForce()[0U] += 1.0e-9;
ASSERT_TRUE(fesa::ResultRecovery::recover(
*freeFixture.model,
*freeFixture.dofs,
*freeFixture.stiffness,
perturbed)
.isOk());
for (const double metric : perturbed.verificationMetrics()) {
EXPECT_GT(metric, 0.0);
EXPECT_LE(metric, 1.0e-10);
}
}
// MITC4-REC-004
TEST(ResultRecovery, UsesGlobalOriginForShellMomentBalance) {
auto centeredDefinition = makeShellDefinition();
auto translatedDefinition = centeredDefinition;
constexpr std::array<double, 3> translation{7.0, 11.0, 0.0};
for (auto& node : translatedDefinition.nodes) {
for (std::size_t component = 0U;
component < translation.size();
++component) {
node.coordinates[component] += translation[component];
}
}
const auto centeredFixture = makeShellFixture(
std::move(centeredDefinition));
const auto translatedFixture = makeShellFixture(
std::move(translatedDefinition));
auto centered = makeShellPhysicalState(centeredFixture);
auto translated = makeShellPhysicalState(translatedFixture);
centered.externalForce()[0U] += 1.0e-9;
translated.externalForce()[0U] += 1.0e-9;
ASSERT_TRUE(fesa::ResultRecovery::recover(
*centeredFixture.model,
*centeredFixture.dofs,
*centeredFixture.stiffness,
centered)
.isOk());
ASSERT_TRUE(fesa::ResultRecovery::recover(
*translatedFixture.model,
*translatedFixture.dofs,
*translatedFixture.stiffness,
translated)
.isOk());
std::array<double, 3> centeredForce{};
for (std::size_t component = 0U; component < 3U; ++component) {
centeredForce[component] = centered.equilibrium()[component];
EXPECT_NEAR(
translated.equilibrium()[component],
centeredForce[component],
1.0e-12);
}
const std::array<double, 3> translatedMomentDelta{
translation[1U] * centeredForce[2U] -
translation[2U] * centeredForce[1U],
translation[2U] * centeredForce[0U] -
translation[0U] * centeredForce[2U],
translation[0U] * centeredForce[1U] -
translation[1U] * centeredForce[0U]};
for (std::size_t component = 0U; component < 3U; ++component) {
EXPECT_NEAR(
translated.equilibrium()[3U + component] -
centered.equilibrium()[3U + component],
translatedMomentDelta[component],
5.0e-12);
}
EXPECT_GT(std::abs(translatedMomentDelta[2U]), 1.0e-9);
}
// MITC4-REC-004
TEST(ResultRecovery, UsesScaleAwareShellMetricsAndRejectsExcess) {
const auto fixture = makeShellFixture(makeShellDefinition());
auto subunit = makeShellPhysicalState(fixture);
auto large = makeShellPhysicalState(fixture);
constexpr double subunitScale = 1.0e-6;
constexpr double largeScale = 1.0e6;
subunit.displacement().scale(subunitScale);
subunit.externalForce().scale(subunitScale);
subunit.externalForce()[0U] += 1.0e-9 * subunitScale;
large.displacement().scale(largeScale);
large.externalForce().scale(largeScale);
large.externalForce()[0U] += 1.0e-9 * largeScale;
ASSERT_TRUE(fesa::ResultRecovery::recover(
*fixture.model,
*fixture.dofs,
*fixture.stiffness,
subunit)
.isOk());
ASSERT_TRUE(fesa::ResultRecovery::recover(
*fixture.model,
*fixture.dofs,
*fixture.stiffness,
large)
.isOk());
for (std::size_t metric = 0U; metric < 3U; ++metric) {
EXPECT_GT(subunit.verificationMetrics()[metric], 0.0);
EXPECT_GT(large.verificationMetrics()[metric], 0.0);
EXPECT_NEAR(
subunit.verificationMetrics()[metric],
large.verificationMetrics()[metric],
1.0e-13);
}
auto constrainedDefinition = makeShellDefinition(false, true);
constrainedDefinition.steps[0U].boundaries[0U].value = 1.0;
const auto constrainedFixture = makeShellFixture(
std::move(constrainedDefinition));
auto rejected = fesa::AnalysisState::create(
*constrainedFixture.dofs, {"Step-1", 0U});
for (std::size_t fullDof = 0U;
fullDof < constrainedFixture.dofs->fullDofCount();
++fullDof) {
rejected.displacement()[fullDof] = 1.0;
}
const std::vector<fesa::CooContribution> unbalancedEntry{
{0U, 0U, 1.0, 0U, 0U}};
auto unbalancedStiffness = fesa::SparseMatrix::fromCoo(
constrainedFixture.dofs->fullDofCount(),
constrainedFixture.dofs->fullDofCount(),
unbalancedEntry,
constrainedFixture.dofs->sparsePattern());
ASSERT_TRUE(unbalancedStiffness.hasValue());
expectStatusCode(
fesa::ResultRecovery::recover(
*constrainedFixture.model,
*constrainedFixture.dofs,
unbalancedStiffness.value(),
rejected),
"global-equilibrium-tolerance-failure");
}
// MITC4-REC-005
TEST(ResultRecovery, InvalidLaterShellLeavesEntirePriorStateUnchanged) {
const auto validFixture = makeShellFixture(makeShellDefinition(true));
auto state = makeShellPhysicalState(validFixture);
ASSERT_TRUE(fesa::ResultRecovery::recover(
*validFixture.model,
*validFixture.dofs,
*validFixture.stiffness,
state)
.isOk());
ASSERT_EQ(state.shellResults().size(), 8U);
const auto priorFirstRow = state.shellResults().front();
const double priorEnergy = state.physicalStrainEnergy();
const auto priorEquilibrium = state.equilibrium();
const auto priorMetrics = state.verificationMetrics();
state.internalForce()[0U] = 91.0;
state.residual()[0U] = 92.0;
state.reaction()[0U] = 93.0;
state.endpointResults().push_back({});
state.displacement()[2U * 6U] =
(std::numeric_limits<double>::max)();
state.displacement()[5U * 6U] =
(std::numeric_limits<double>::max)();
state.externalForce() = fesa::Vector{validFixture.dofs->fullDofCount()};
auto zeroStiffness = fesa::SparseMatrix::fromCoo(
validFixture.dofs->fullDofCount(),
validFixture.dofs->fullDofCount(),
{},
validFixture.dofs->sparsePattern());
ASSERT_TRUE(zeroStiffness.hasValue());
const auto status = fesa::ResultRecovery::recover(
*validFixture.model,
*validFixture.dofs,
zeroStiffness.value(),
state);
expectStatusCode(status, "invalid-shell-recovery");
EXPECT_DOUBLE_EQ(state.internalForce()[0U], 91.0);
EXPECT_DOUBLE_EQ(state.residual()[0U], 92.0);
EXPECT_DOUBLE_EQ(state.reaction()[0U], 93.0);
EXPECT_EQ(state.endpointResults().size(), 1U);
ASSERT_EQ(state.shellResults().size(), 8U);
EXPECT_EQ(state.shellResults().front().element, priorFirstRow.element);
EXPECT_EQ(state.shellResults().front().location, priorFirstRow.location);
EXPECT_EQ(
state.shellResults().front().generalizedStrain,
priorFirstRow.generalizedStrain);
EXPECT_EQ(
state.shellResults().front().sectionResultant,
priorFirstRow.sectionResultant);
EXPECT_DOUBLE_EQ(state.physicalStrainEnergy(), priorEnergy);
EXPECT_EQ(state.equilibrium(), priorEquilibrium);
EXPECT_EQ(state.verificationMetrics(), priorMetrics);
}