feat(cpp-object-oriented-modular-refactoring): step 17 - generic-result-recovery

This commit is contained in:
KOKO\Mimi
2026-08-16 10:40:49 +09:00
parent 31b6129cf8
commit d711e6d4fd
3 changed files with 543 additions and 273 deletions
+289
View File
@@ -7,6 +7,7 @@
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <limits>
#include <memory>
#include <stdexcept>
@@ -19,6 +20,7 @@
#include "fesa/assembly/load_assembler.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/assembly/sparse_assembler.h"
#include "fesa/elements/element.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/vector3.h"
#include "fesa/model/domain.h"
@@ -43,6 +45,141 @@ struct ShellRecoveryFixture {
std::unique_ptr<fesa::SparseMatrix> stiffness;
};
class FakeRecoveryElement final : public fesa::Element {
public:
FakeRecoveryElement(fesa::ElementDofLayout layout,
fesa::ElementResultBundle bundle,
const bool fail_recovery = false)
: layout_{std::move(layout)},
bundle_{std::move(bundle)},
fail_recovery_{fail_recovery} {}
const fesa::ElementDofLayout& DofLayout() const noexcept override {
return layout_;
}
fesa::Result<fesa::ElementStiffnessContribution> ComputeStiffness()
const override {
const std::size_t local_dof_count =
layout_.node_indices.size() * layout_.components_per_node.size();
return fesa::Result<fesa::ElementStiffnessContribution>::Success(
{layout_, fesa::Matrix{local_dof_count, local_dof_count}});
}
fesa::Result<fesa::ElementResultBundle> Recover(
const fesa::Vector& element_displacement) const override {
observed_displacement_ = element_displacement;
if (fail_recovery_) {
return fesa::Result<fesa::ElementResultBundle>::Failure(
fesa::Status::Failure(
fesa::FailureCategory::kModel,
{{fesa::Severity::kError,
"fake-recovery-failure",
{},
"RESULT_RECOVERY",
layout_.source_id.source_label_text,
"The fake runtime element rejected recovery."}}));
}
return fesa::Result<fesa::ElementResultBundle>::Success(bundle_);
}
const fesa::Vector& ObservedDisplacement() const noexcept {
return observed_displacement_;
}
private:
fesa::ElementDofLayout layout_;
fesa::ElementResultBundle bundle_;
bool fail_recovery_;
mutable fesa::Vector observed_displacement_{0U};
};
std::vector<fesa::DofComponent> FullNodeComponents() {
return {fesa::DofComponent::kUx, fesa::DofComponent::kUy,
fesa::DofComponent::kUz, fesa::DofComponent::kUrx,
fesa::DofComponent::kUry, fesa::DofComponent::kUrz};
}
fesa::ElementDofLayout MakeRuntimeLayout(const fesa::Domain& domain,
const fesa::EntityIndex element) {
const auto& definition = domain.Elements()[element];
return {definition.SourceId(), definition.NodeIndices(),
FullNodeComponents()};
}
fesa::ElementResultBundle MakeFakeBeamBundle(const fesa::Domain& domain,
const fesa::EntityIndex element,
const double value) {
const auto& definition = domain.Elements()[element];
fesa::BeamElementResultRows rows{};
rows.endpoint_rows = {{element,
0,
domain.Nodes()[definition.NodeIndices()[0U]].source_id,
{-value, 0.0, 0.0, 0.0, 0.0, 0.0},
{value, 2.0 * value, 3.0 * value, 4.0 * value}},
{element,
1,
domain.Nodes()[definition.NodeIndices()[1U]].source_id,
{value, 0.0, 0.0, 0.0, 0.0, 0.0},
{value, 2.0 * value, 3.0 * value, 4.0 * value}}};
rows.gauss_rows = {{element,
1,
{value, 0.0, 0.0, 0.0},
{value, 2.0 * value, 3.0 * value, 4.0 * value}},
{element,
2,
{value + 0.5, 0.0, 0.0, 0.0},
{value, 2.0 * value, 3.0 * value, 4.0 * value}}};
rows.stress_rows = {{element, 1, 0U, 0.0, 0.0, 5.0 * value, "fake"},
{element, 2, 0U, 0.0, 0.0, 6.0 * value, "fake"}};
return {definition.SourceId(), std::move(rows)};
}
fesa::ElementResultBundle MakeFakeShellBundle(const fesa::Domain& domain,
const fesa::EntityIndex element,
const double physical_energy) {
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<fesa::ShellMidsurfaceLocation, 4> locations{
fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2,
fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4};
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}};
const std::array<fesa::ShellSectionPosition, 3> positions{
fesa::ShellSectionPosition::kBottom, fesa::ShellSectionPosition::kMiddle,
fesa::ShellSectionPosition::kTop};
constexpr std::array<double, 3> zeta{-1.0, 0.0, 1.0};
fesa::ShellElementResultRows rows{};
rows.physical_strain_energy = physical_energy;
for (std::size_t point = 0U; point < locations.size(); ++point) {
fesa::ShellResultRow row{};
row.element = element;
row.location = locations[point];
row.natural_coordinates = coordinates[point];
row.local_frame = {{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
row.generalized_strain[0U] = static_cast<double>(point + 1U);
row.section_resultant[0U] = 10.0 * static_cast<double>(point + 1U);
for (std::size_t position = 0U; position < positions.size(); ++position) {
row.stress[position] = {
positions[position],
zeta[position],
{static_cast<double>(point + position + 1U), 0.0, 0.0}};
}
rows.rows.push_back(std::move(row));
}
return {domain.Elements()[element].SourceId(), std::move(rows)};
}
void ExpectVectorEqual(const fesa::Vector& actual,
const fesa::Vector& expected) {
ASSERT_EQ(actual.Size(), expected.Size());
for (std::size_t index = 0U; index < actual.Size(); ++index) {
EXPECT_DOUBLE_EQ(actual[index], expected[index]);
}
}
fesa::ModelDefinition MakeDefinition(
const bool two_elements = false,
std::vector<std::array<double, 2>> section_points = {},
@@ -343,6 +480,158 @@ std::vector<fesa::EndpointResultRow> MakeStationRows(
} // namespace
// C-RECOVERY-001
TEST(ResultRecovery, AggregatesFakeBeamBundlesInStableRuntimeOrder) {
const auto fixture = MakeFixture(true);
const auto input = MakeAxialEquilibriumState(fixture);
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
FakeRecoveryElement first{MakeRuntimeLayout(*fixture.domain, 0U),
MakeFakeBeamBundle(*fixture.domain, 0U, 1.0)};
FakeRecoveryElement second{MakeRuntimeLayout(*fixture.domain, 1U),
MakeFakeBeamBundle(*fixture.domain, 1U, 2.0)};
const fesa::ElementView elements{std::cref(first), std::cref(second)};
const fesa::Status status = fesa::ResultRecovery::Recover(
*fixture.model, elements, *fixture.dofs, *fixture.stiffness,
input.Displacement(), input.ExternalForce(), state);
ASSERT_TRUE(status.IsOk());
ExpectVectorEqual(state.Displacement(), input.Displacement());
ExpectVectorEqual(state.ExternalForce(), input.ExternalForce());
ASSERT_EQ(first.ObservedDisplacement().Size(), 12U);
EXPECT_DOUBLE_EQ(first.ObservedDisplacement()[0U], 0.1);
EXPECT_DOUBLE_EQ(first.ObservedDisplacement()[6U], 0.3);
ASSERT_EQ(second.ObservedDisplacement().Size(), 12U);
EXPECT_DOUBLE_EQ(second.ObservedDisplacement()[0U], 0.3);
EXPECT_DOUBLE_EQ(second.ObservedDisplacement()[6U], 0.0);
ASSERT_EQ(state.EndpointResults().size(), 4U);
ASSERT_EQ(state.GaussResults().size(), 4U);
ASSERT_EQ(state.StressResults().size(), 4U);
for (std::size_t element = 0U; element < 2U; ++element) {
const double value = static_cast<double>(element + 1U);
const auto& negative_endpoint = state.EndpointResults()[2U * element];
const auto& positive_endpoint = state.EndpointResults()[2U * element + 1U];
EXPECT_EQ(negative_endpoint.element, element);
EXPECT_EQ(negative_endpoint.endpoint, 0);
EXPECT_DOUBLE_EQ(negative_endpoint.end_action[0U], -value);
EXPECT_DOUBLE_EQ(negative_endpoint.section_resultant[0U], value);
EXPECT_EQ(positive_endpoint.element, element);
EXPECT_EQ(positive_endpoint.endpoint, 1);
EXPECT_DOUBLE_EQ(positive_endpoint.end_action[0U], value);
EXPECT_DOUBLE_EQ(positive_endpoint.section_resultant[0U], value);
EXPECT_EQ(state.GaussResults()[2U * element].element, element);
EXPECT_EQ(state.GaussResults()[2U * element].gauss_point, 1);
EXPECT_EQ(state.GaussResults()[2U * element + 1U].element, element);
EXPECT_EQ(state.GaussResults()[2U * element + 1U].gauss_point, 2);
EXPECT_EQ(state.StressResults()[2U * element].element, element);
EXPECT_EQ(state.StressResults()[2U * element + 1U].element, element);
}
EXPECT_TRUE(state.ShellResults().empty());
}
// C-RECOVERY-001
TEST(ResultRecovery, AggregatesFakeShellBundleAndPhysicalEnergy) {
const auto fixture = MakeShellFixture(MakeShellDefinition());
const auto input = MakeShellPhysicalState(fixture);
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
constexpr double physical_energy = 37.5;
FakeRecoveryElement shell{
MakeRuntimeLayout(*fixture.domain, 0U),
MakeFakeShellBundle(*fixture.domain, 0U, physical_energy)};
const fesa::ElementView elements{std::cref(shell)};
const fesa::Status status = fesa::ResultRecovery::Recover(
*fixture.model, elements, *fixture.dofs, *fixture.stiffness,
input.Displacement(), input.ExternalForce(), state);
ASSERT_TRUE(status.IsOk());
ExpectVectorEqual(state.Displacement(), input.Displacement());
ExpectVectorEqual(state.ExternalForce(), input.ExternalForce());
EXPECT_TRUE(state.EndpointResults().empty());
EXPECT_TRUE(state.GaussResults().empty());
EXPECT_TRUE(state.StressResults().empty());
ASSERT_EQ(state.ShellResults().size(), 4U);
EXPECT_EQ(state.ShellResults()[0U].location,
fesa::ShellMidsurfaceLocation::kGp1);
EXPECT_EQ(state.ShellResults()[3U].location,
fesa::ShellMidsurfaceLocation::kGp4);
EXPECT_DOUBLE_EQ(state.ShellResults()[0U].generalized_strain[0U], 1.0);
EXPECT_DOUBLE_EQ(state.ShellResults()[3U].section_resultant[0U], 40.0);
EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), physical_energy);
}
// C-RECOVERY-001
TEST(ResultRecovery, FailingFakeBundleRollsBackTheWholeState) {
const auto fixture = MakeFixture(true);
const auto input = MakeAxialEquilibriumState(fixture);
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
for (std::size_t full_dof = 0U; full_dof < fixture.dofs->FullDofCount();
++full_dof) {
state.Displacement()[full_dof] = static_cast<double>(full_dof) + 0.25;
state.ExternalForce()[full_dof] = -static_cast<double>(full_dof) - 0.5;
state.InternalForce()[full_dof] = 100.0 + static_cast<double>(full_dof);
state.Residual()[full_dof] = 200.0 + static_cast<double>(full_dof);
state.Reaction()[full_dof] = 300.0 + static_cast<double>(full_dof);
}
auto stale_beam_bundle = MakeFakeBeamBundle(*fixture.domain, 0U, 9.0);
auto stale_beam_rows =
std::get<fesa::BeamElementResultRows>(stale_beam_bundle.payload);
state.EndpointResults() = std::move(stale_beam_rows.endpoint_rows);
state.GaussResults() = std::move(stale_beam_rows.gauss_rows);
state.StressResults() = std::move(stale_beam_rows.stress_rows);
auto stale_shell_bundle = MakeFakeShellBundle(*fixture.domain, 0U, 71.0);
const auto& stale_shell_rows =
std::get<fesa::ShellElementResultRows>(stale_shell_bundle.payload);
fesa::ShellStateCandidate stale_shell_candidate{};
stale_shell_candidate.rows = stale_shell_rows.rows;
stale_shell_candidate.physical_strain_energy =
stale_shell_rows.physical_strain_energy;
stale_shell_candidate.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
stale_shell_candidate.verification_metrics = {1.0e-11, 2.0e-11, 3.0e-11};
ASSERT_TRUE(
state.CommitShellResults({0U}, std::move(stale_shell_candidate)).IsOk());
const fesa::AnalysisState prior = state;
FakeRecoveryElement first{MakeRuntimeLayout(*fixture.domain, 0U),
MakeFakeBeamBundle(*fixture.domain, 0U, 1.0)};
FakeRecoveryElement failing{MakeRuntimeLayout(*fixture.domain, 1U),
MakeFakeBeamBundle(*fixture.domain, 1U, 2.0),
true};
const fesa::ElementView elements{std::cref(first), std::cref(failing)};
const fesa::Status status = fesa::ResultRecovery::Recover(
*fixture.model, elements, *fixture.dofs, *fixture.stiffness,
input.Displacement(), input.ExternalForce(), state);
ExpectStatusCode(status, "fake-recovery-failure");
ExpectVectorEqual(state.Displacement(), prior.Displacement());
ExpectVectorEqual(state.ExternalForce(), prior.ExternalForce());
ExpectVectorEqual(state.InternalForce(), prior.InternalForce());
ExpectVectorEqual(state.Residual(), prior.Residual());
ExpectVectorEqual(state.Reaction(), prior.Reaction());
EXPECT_EQ(state.Identity().step_name, prior.Identity().step_name);
EXPECT_EQ(state.Identity().frame_index, prior.Identity().frame_index);
ASSERT_EQ(state.EndpointResults().size(), prior.EndpointResults().size());
EXPECT_EQ(state.EndpointResults()[0U].element,
prior.EndpointResults()[0U].element);
EXPECT_EQ(state.EndpointResults()[0U].end_action,
prior.EndpointResults()[0U].end_action);
ASSERT_EQ(state.GaussResults().size(), prior.GaussResults().size());
EXPECT_EQ(state.GaussResults()[0U].generalized_resultant,
prior.GaussResults()[0U].generalized_resultant);
ASSERT_EQ(state.StressResults().size(), prior.StressResults().size());
EXPECT_DOUBLE_EQ(state.StressResults()[0U].s11,
prior.StressResults()[0U].s11);
ASSERT_EQ(state.ShellResults().size(), prior.ShellResults().size());
EXPECT_EQ(state.ShellResults()[0U].generalized_strain,
prior.ShellResults()[0U].generalized_strain);
EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), prior.PhysicalStrainEnergy());
EXPECT_EQ(state.Equilibrium(), prior.Equilibrium());
EXPECT_EQ(state.VerificationMetrics(), prior.VerificationMetrics());
}
TEST(ResultRecovery, ComputesResidualReactionForNonzeroPrescription) {
const auto fixture = MakeFixture();
auto state = MakeAxialEquilibriumState(fixture);