feat: implement consistent-mass HHT shell dynamics

This commit is contained in:
KOKO\Mimi
2026-09-22 21:38:42 +09:00
parent fec8cf09c2
commit f0dc42a280
41 changed files with 9294 additions and 125 deletions
@@ -0,0 +1,305 @@
#include "fesa/analysis/dynamic_analysis_state.h"
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/fem/dof_manager.h"
namespace {
constexpr std::array<std::array<double, 3>, 3> kIdentityTriad{
std::array<double, 3>{1.0, 0.0, 0.0}, std::array<double, 3>{0.0, 1.0, 0.0},
std::array<double, 3>{0.0, 0.0, 1.0}};
fesa::ModelDefinition MakeDefinition(const bool fixed = true) {
const std::filesystem::path source{"dynamic-state.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "hht-output-unit-fixture";
definition.nodes = {{{"Shell-A", 10, "0010"}, {0.0, 0.0, 0.0}, {source, 1U}},
{{"Shell-A", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-A", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"Shell-A", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Material", 1000.0, 0.25, {source, 5U}}};
definition.densities = {{0U, 2.0, {source, 6U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 7U}}};
definition.shell_elements = {{{"Shell-A", 9, "0009"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 8U}}};
for (std::size_t node = 0U; node < 4U; ++node) {
definition.shell_node_initial_frames.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}});
}
definition.node_sets = {{"All", std::nullopt, {0U, 1U, 2U, 3U}, {source, 9U}},
{"Loaded", std::nullopt, {0U}, {source, 10U}}};
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "Transient Load";
step.source_step_name_canonical = "TRANSIENT LOAD";
step.initial_increment = 0.25;
step.time_period = 0.5;
step.increment_count = 2U;
step.maximum_increments = 3U;
if (fixed) {
step.boundaries = {{"All", 1, 6, 0.0, {source, 11U}}};
step.loads = {{"Loaded", 3, 4.0, {source, 12U}}};
}
definition.dynamic_step = step;
return definition;
}
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
fesa::DynamicAnalysisState state;
};
Fixture MakeFixture(fesa::ModelDefinition definition = MakeDefinition()) {
auto domain_result = fesa::Domain::Create(std::move(definition));
if (!domain_result.HasValue())
throw std::runtime_error{"Invalid fixture Domain."};
auto domain =
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model = fesa::AnalysisModel::Create(*domain);
if (!model.HasValue())
throw std::runtime_error{"Invalid fixture AnalysisModel."};
auto dofs = fesa::DofManager::Create(model.Value());
if (!dofs.HasValue()) throw std::runtime_error{"Invalid fixture DofManager."};
auto state = fesa::DynamicAnalysisState::Create(*domain, dofs.Value());
if (!state.HasValue()) throw std::runtime_error{"Invalid fixture state."};
return {std::move(domain), std::move(state.Value())};
}
// A fully restrained, initially undeformed shell under a dead nodal force has
// zero motion and strain, and nonzero support force at every accepted endpoint.
fesa::DynamicAcceptedFrameState MakeFrame(const fesa::Domain& domain,
const std::uint32_t index) {
fesa::DynamicAcceptedFrameState frame{};
frame.frame_index = index;
frame.increment_index = index;
frame.time = static_cast<double>(index) * domain.Dynamic()->initial_increment;
frame.is_initial = index == 0U;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
frame.nodal_internal_order.push_back(static_cast<fesa::EntityIndex>(node));
frame.nodal_source_ids.push_back(domain.Nodes()[node].source_id);
frame.nodal_triad.push_back(kIdentityTriad);
frame.nodal_director.push_back({0.0, 0.0, 1.0});
}
for (auto* vector :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.inertia_force,
&frame.internal_force, &frame.external_force, &frame.residual,
&frame.reaction, &frame.residual_scale}) {
*vector = fesa::Vector{24U};
}
frame.angular_velocity = fesa::Vector{12U};
frame.angular_acceleration = fesa::Vector{12U};
if (!domain.Dynamic()->loads.empty()) {
frame.external_force[2U] = 4.0;
frame.residual[2U] = -4.0;
frame.reaction[2U] = -4.0;
frame.residual_scale[2U] = 4.0;
}
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<std::array<double, 2>, 4> natural{
std::array<double, 2>{-gauss, -gauss},
{gauss, -gauss},
{gauss, gauss},
{-gauss, gauss}};
const std::array<fesa::ShellMidsurfaceLocation, 4> locations{
fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2,
fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4};
for (std::size_t point = 0U; point < 4U; ++point) {
frame.shell_results.push_back(
{0U,
locations[point],
natural[point],
kIdentityTriad,
{},
{},
{{{fesa::ShellSectionPosition::kBottom, -1.0, {}},
{fesa::ShellSectionPosition::kMiddle, 0.0, {}},
{fesa::ShellSectionPosition::kTop, 1.0, {}}}}});
}
return frame;
}
TEST(DynamicAnalysisState, DefaultStateRejectsTrialAndCommit) {
fesa::DynamicAnalysisState state;
EXPECT_FALSE(state.IsInitialized());
EXPECT_TRUE(state.AcceptedFrames().empty());
EXPECT_FALSE(state.BeginTrialFromCommitted().HasValue());
EXPECT_THROW((void)state.CommittedState(), std::logic_error);
const auto status = state.CommitAcceptedFrame({});
EXPECT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "dynamic-invalid-state");
}
TEST(DynamicAnalysisState,
CommitsCompleteZeroCorrectionHistoryAndCopiesTrials) {
auto fixture = MakeFixture();
EXPECT_TRUE(fixture.state.IsInitialized());
EXPECT_FALSE(fixture.state.BeginTrialFromCommitted().HasValue());
for (std::uint32_t index = 0U; index <= 2U; ++index) {
auto frame = MakeFrame(*fixture.domain, index);
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
frame.reaction[2U] = 123.0;
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().reaction[2U], -4.0);
EXPECT_EQ(fixture.state.AcceptedFrames().size(), index + 1U);
}
auto trial = fixture.state.BeginTrialFromCommitted();
ASSERT_TRUE(trial.HasValue());
trial.Value().generalized_coordinates[0U] = 1.0;
trial.Value().shell_results[0U].stress[0U].components[0U] = 1.0;
fixture.state.DiscardTrial();
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().generalized_coordinates[0U],
0.0);
EXPECT_DOUBLE_EQ(fixture.state.CommittedState()
.shell_results[0U]
.stress[0U]
.components[0U],
0.0);
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().time, 0.5);
EXPECT_FALSE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 3U)).IsOk());
EXPECT_EQ(fixture.state.AcceptedFrames().size(), 3U);
}
TEST(DynamicAnalysisState,
RejectsMalformedInventoryScheduleAndPreservesEndpoint) {
auto fixture = MakeFixture();
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
using Frame = fesa::DynamicAcceptedFrameState;
const std::vector<std::function<void(Frame&)>> mutations{
[](Frame& frame) { frame.frame_index = 2U; },
[](Frame& frame) { frame.increment_index = 2U; },
[](Frame& frame) { frame.time = 0.26; },
[](Frame& frame) {
frame.time = std::numeric_limits<double>::quiet_NaN();
},
[](Frame& frame) { frame.is_initial = true; },
[](Frame& frame) { frame.iteration_count = 101U; },
[](Frame& frame) { frame.nodal_internal_order[1U] = 0U; },
[](Frame& frame) {
frame.nodal_source_ids[1U].source_label_text = "wrong";
},
[](Frame& frame) { frame.nodal_triad.pop_back(); },
[](Frame& frame) {
frame.nodal_director[0U][2U] = std::numeric_limits<double>::infinity();
},
[](Frame& frame) { frame.acceleration = fesa::Vector{23U}; },
[](Frame& frame) { frame.velocity[0U] = 1.0; },
[](Frame& frame) { frame.acceleration[0U] = 1.0; },
[](Frame& frame) { frame.algorithmic_acceleration[0U] = 1.0; },
[](Frame& frame) { frame.generalized_coordinates[0U] = 1.0; },
[](Frame& frame) { frame.reaction[2U] = 0.0; },
[](Frame& frame) { frame.residual[2U] = 0.0; },
[](Frame& frame) { frame.residual_scale[0U] = -1.0; },
[](Frame& frame) {
frame.external_force[0U] = 1.0;
frame.residual[0U] = -1.0;
frame.reaction[0U] = -1.0;
},
[](Frame& frame) { frame.shell_results.pop_back(); },
[](Frame& frame) { frame.shell_results[0U].element = 1U; },
[](Frame& frame) {
frame.shell_results[0U].natural_coordinates[0U] = 0.0;
},
[](Frame& frame) { frame.shell_results[0U].stress[0U].zeta = 0.0; },
[](Frame& frame) {
frame.shell_results[0U].stress[0U].components[0U] =
std::numeric_limits<double>::quiet_NaN();
},
[](Frame& frame) {
frame.kinetic_energy = std::numeric_limits<double>::infinity();
},
[](Frame& frame) {
frame.angular_momentum_rate[0U] =
std::numeric_limits<double>::quiet_NaN();
},
[](Frame& frame) { frame.hidden_constraint = fesa::Vector{1U}; },
[](Frame& frame) { frame.free_residual_metrics[0U] = -1.0; }};
for (std::size_t index = 0U; index < mutations.size(); ++index) {
SCOPED_TRACE(index);
auto frame = MakeFrame(*fixture.domain, 1U);
mutations[index](frame);
const auto status = fixture.state.CommitAcceptedFrame(frame);
ASSERT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "dynamic-invalid-state");
ASSERT_EQ(fixture.state.AcceptedFrames().size(), 1U);
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().time, 0.0);
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().reaction[2U], -4.0);
}
EXPECT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 1U)).IsOk());
}
TEST(DynamicAnalysisState, RejectsInvalidInitialStateAndFreeReaction) {
auto fixture = MakeFixture(MakeDefinition(false));
auto initial = MakeFrame(*fixture.domain, 0U);
initial.generalized_coordinates[0U] = 1.0;
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
initial = MakeFrame(*fixture.domain, 0U);
initial.acceleration[0U] = 1.0;
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
initial = MakeFrame(*fixture.domain, 0U);
initial.time = -0.0;
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
initial = MakeFrame(*fixture.domain, 0U);
initial.reaction[0U] = 1.0;
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
EXPECT_TRUE(fixture.state.AcceptedFrames().empty());
EXPECT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
}
TEST(DynamicAnalysisState, CreateRejectsInvalidScheduleAndNonzeroBoundary) {
const std::vector<std::function<void(fesa::ModelDefinition&)>> mutations{
[](auto& definition) { definition.dynamic_step->increment_count = 0U; },
[](auto& definition) { definition.dynamic_step->time_period = 0.6; },
[](auto& definition) {
definition.dynamic_step->maximum_increments = 1U;
},
[](auto& definition) {
definition.dynamic_step->initial_increment =
std::numeric_limits<double>::quiet_NaN();
},
[](auto& definition) {
definition.dynamic_step->boundaries[0U].value = 1.0;
}};
for (std::size_t index = 0U; index < mutations.size(); ++index) {
SCOPED_TRACE(index);
auto definition = MakeDefinition();
mutations[index](definition);
auto domain = fesa::Domain::Create(std::move(definition));
ASSERT_TRUE(domain.HasValue());
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
auto dofs = fesa::DofManager::Create(model.Value());
ASSERT_TRUE(dofs.HasValue());
EXPECT_FALSE(
fesa::DynamicAnalysisState::Create(domain.Value(), dofs.Value())
.HasValue());
}
}
} // namespace