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,406 @@
#include "fesa/analysis/dynamic_analysis_state.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#include "fesa/elements/element_definition.h"
#include "fesa/elements/finite_rotation.h"
namespace fesa {
namespace {
constexpr std::size_t kDofsPerNode = 6U;
constexpr std::size_t kAngularDofsPerNode = 3U;
constexpr std::size_t kShellLocationsPerElement = 4U;
constexpr std::uint32_t kMaximumNewtonIterations = 100U;
/// @brief Reports a rejected state without changing committed history.
Status Failure(const FailureCategory category, const std::string& message) {
return Status::Failure(category, {{Severity::kError,
"dynamic-invalid-state",
{},
"DYNAMIC_ANALYSIS_STATE",
{},
message}});
}
/// @brief Compares canonical and raw source identity exactly.
bool SameId(const SourceEntityId& left, const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
/// @brief Checks all components of one fixed-size physical record.
template <std::size_t Size>
bool IsFinite(const std::array<double, Size>& values) {
return std::all_of(values.begin(), values.end(),
[](double value) { return std::isfinite(value); });
}
/// @brief Checks every vector component before state acceptance.
bool IsFinite(const Vector& values) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return false;
}
}
return true;
}
/// @brief Checks all physical recovery quantities for finite values.
bool IsFinite(const ShellResultRow& row) {
if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) ||
!IsFinite(row.section_resultant)) {
return false;
}
for (const auto& axis : row.local_frame) {
if (!IsFinite(axis)) {
return false;
}
}
return std::all_of(row.stress.begin(), row.stress.end(),
[](const ShellSectionStressRow& stress) {
return std::isfinite(stress.zeta) &&
IsFinite(stress.components);
});
}
/// @brief Collects stable Domain indices for recovery identity.
std::vector<EntityIndex> ShellElementOrder(const Domain& domain) {
std::vector<EntityIndex> order;
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) {
order.push_back(static_cast<EntityIndex>(index));
}
}
return order;
}
/// @brief Requires complete ordered Gauss and section-position rows.
Status ValidateShellRows(const DynamicAcceptedFrameState& frame,
const std::vector<EntityIndex>& shell_order) {
if (shell_order.size() > (std::numeric_limits<std::size_t>::max)() /
kShellLocationsPerElement ||
frame.shell_results.size() !=
shell_order.size() * kShellLocationsPerElement) {
return Failure(FailureCategory::kSolver,
"Shell recovery inventory does not match the Domain.");
}
constexpr std::array<ShellMidsurfaceLocation, kShellLocationsPerElement>
kLocations{ShellMidsurfaceLocation::kGp1, ShellMidsurfaceLocation::kGp2,
ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4};
constexpr std::array<ShellSectionPosition, 3U> kPositions{
ShellSectionPosition::kBottom, ShellSectionPosition::kMiddle,
ShellSectionPosition::kTop};
constexpr std::array<double, 3U> kZeta{-1.0, 0.0, 1.0};
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<std::array<double, 2U>, 4U> natural{
std::array<double, 2U>{-gauss, -gauss},
{gauss, -gauss},
{gauss, gauss},
{-gauss, gauss}};
for (std::size_t element = 0U; element < shell_order.size(); ++element) {
for (std::size_t point = 0U; point < kShellLocationsPerElement; ++point) {
const auto& row =
frame.shell_results[element * kShellLocationsPerElement + point];
if (row.element != shell_order[element] ||
row.location != kLocations[point] ||
row.natural_coordinates != natural[point] || !IsFinite(row)) {
return Failure(FailureCategory::kSolver,
"Shell recovery rows have invalid identity or values.");
}
for (std::size_t position = 0U; position < kPositions.size();
++position) {
if (row.stress[position].position != kPositions[position] ||
row.stress[position].zeta != kZeta[position]) {
return Failure(FailureCategory::kSolver,
"Shell stress rows have invalid section identity.");
}
}
}
}
return Status::Ok();
}
} // namespace
Result<DynamicAnalysisState> DynamicAnalysisState::Create(
const Domain& domain, const DofManager& dofs) {
const DynamicStepDefinition* const dynamic = domain.Dynamic();
const std::size_t node_count = domain.Nodes().size();
if (dynamic == nullptr || dynamic->increment_count == 0U ||
!std::isfinite(dynamic->initial_increment) ||
!std::isfinite(dynamic->time_period) ||
dynamic->initial_increment <= 0.0 ||
dynamic->increment_count > dynamic->maximum_increments ||
!std::isfinite(dynamic->alpha) || dynamic->alpha < -1.0 / 3.0 ||
dynamic->alpha > 0.0 ||
dynamic->beta != 0.25 * (1.0 - dynamic->alpha) * (1.0 - dynamic->alpha) ||
dynamic->gamma != 0.5 - dynamic->alpha || dynamic->time_period <= 0.0 ||
static_cast<double>(dynamic->increment_count) *
dynamic->initial_increment !=
dynamic->time_period ||
node_count == 0U || domain.ShellElements().Empty() ||
domain.Elements().Size() != domain.ShellElements().Size() ||
domain.ShellNodeInitialFrames().size() != node_count ||
node_count > (std::numeric_limits<std::size_t>::max)() / kDofsPerNode ||
dofs.FullDofCount() != node_count * kDofsPerNode ||
!dofs.ValidateInvariants().IsOk()) {
return Result<DynamicAnalysisState>::Failure(
Failure(FailureCategory::kModel,
"Dynamic schedule and full-DOF inventory must be valid."));
}
for (std::size_t index = 0U; index < dofs.PrescribedValues().Size();
++index) {
if (dofs.PrescribedValues()[index] != 0.0) {
return Result<DynamicAnalysisState>::Failure(
Failure(FailureCategory::kModel,
"Dynamic prescribed components must be exactly zero."));
}
}
std::vector<SourceEntityId> source_ids;
source_ids.reserve(node_count);
for (const Node& node : domain.Nodes()) {
source_ids.push_back(node.source_id);
}
return Result<DynamicAnalysisState>::Success(DynamicAnalysisState{
dofs.FullDofCount(), dynamic->increment_count, dynamic->initial_increment,
ShellElementOrder(domain), dofs.ConstrainedDofs(), std::move(source_ids),
domain.ShellNodeInitialFrames()});
}
Result<DynamicAcceptedFrameState>
DynamicAnalysisState::BeginTrialFromCommitted() const {
if (accepted_frames_.empty()) {
return Result<DynamicAcceptedFrameState>::Failure(
Failure(FailureCategory::kSolver,
"A dynamic trial requires an accepted frame zero."));
}
return Result<DynamicAcceptedFrameState>::Success(accepted_frames_.back());
}
Status DynamicAnalysisState::CommitAcceptedFrame(
const DynamicAcceptedFrameState& frame) {
if (!initialized_) {
return Failure(FailureCategory::kSolver,
"A default dynamic state cannot accept frames.");
}
const std::size_t expected_frame = accepted_frames_.size();
if (expected_frame > (std::numeric_limits<std::uint32_t>::max)() ||
frame.frame_index != expected_frame ||
frame.increment_index != expected_frame ||
frame.increment_index > increment_count_ || !std::isfinite(frame.time) ||
frame.time !=
static_cast<double>(frame.increment_index) * initial_increment_ ||
frame.is_initial != (frame.frame_index == 0U) ||
(frame.is_initial && frame.iteration_count != 0U) ||
frame.iteration_count > kMaximumNewtonIterations ||
(frame.is_initial && std::signbit(frame.time))) {
return Failure(FailureCategory::kSolver,
"Frame schedule or iteration identity is invalid.");
}
if (!accepted_frames_.empty() && frame.time <= accepted_frames_.back().time) {
return Failure(FailureCategory::kSolver,
"Accepted dynamic frame times must strictly increase.");
}
const std::size_t node_count = nodal_source_ids_.size();
if (frame.nodal_internal_order.size() != node_count ||
frame.nodal_source_ids.size() != node_count ||
frame.nodal_triad.size() != node_count ||
frame.nodal_director.size() != node_count) {
return Failure(FailureCategory::kSolver,
"Frame node inventory does not match the Domain.");
}
for (std::size_t node = 0U; node < node_count; ++node) {
if (frame.nodal_internal_order[node] != node ||
!SameId(frame.nodal_source_ids[node], nodal_source_ids_[node]) ||
!IsFinite(frame.nodal_triad[node][0U]) ||
!IsFinite(frame.nodal_triad[node][1U]) ||
!IsFinite(frame.nodal_triad[node][2U]) ||
!IsFinite(frame.nodal_director[node])) {
return Failure(FailureCategory::kSolver,
"Frame nodal identity or orientation is invalid.");
}
}
const std::array<const Vector*, 10U> full_vectors{
&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};
for (const Vector* values : full_vectors) {
if (values->Size() != full_dof_count_ || !IsFinite(*values)) {
return Failure(FailureCategory::kSolver,
"Frame generalized vector inventory is invalid.");
}
}
// N5 preserves the existing chart and 1e-12 orientation consistency checks.
for (std::size_t node = 0U; node < node_count; ++node) {
Matrix initial{3U, 3U};
for (std::size_t component = 0U; component < 3U; ++component) {
initial(component, 0U) = initial_frames_[node].tangent_a[component];
initial(component, 1U) = initial_frames_[node].tangent_b[component];
initial(component, 2U) = initial_frames_[node].director[component];
}
const std::size_t offset = node * kDofsPerNode;
const Vector3 phi{frame.generalized_coordinates[offset + 3U],
frame.generalized_coordinates[offset + 4U],
frame.generalized_coordinates[offset + 5U]};
const auto rotation = MakeFiniteRotationState(phi, initial);
if (!rotation.HasValue()) {
return Failure(FailureCategory::kSolver,
"Frame rotation is outside the valid initial chart.");
}
for (std::size_t axis = 0U; axis < 3U; ++axis) {
for (std::size_t component = 0U; component < 3U; ++component) {
if (std::abs(frame.nodal_triad[node][axis][component] -
rotation.Value().q(component, axis)) > 1.0e-12) {
return Failure(FailureCategory::kSolver,
"Frame triad does not match its rotation coordinate.");
}
}
if (std::abs(frame.nodal_director[node][axis] -
rotation.Value().q(axis, 2U)) > 1.0e-12) {
return Failure(
FailureCategory::kSolver,
"Frame director does not match its rotation coordinate.");
}
}
}
const std::array<const Vector*, 6U> hidden_vectors{
&frame.hidden_constraint,
&frame.hidden_velocity_defect,
&frame.hidden_acceleration_defect,
&frame.hidden_constraint_scale,
&frame.hidden_velocity_defect_scale,
&frame.hidden_acceleration_defect_scale};
for (const Vector* values : hidden_vectors) {
if (values->Size() != frame.hidden_constraint.Size() ||
!IsFinite(*values)) {
return Failure(FailureCategory::kSolver,
"Frame hidden-constraint evidence has invalid size.");
}
}
if ((!accepted_frames_.empty() &&
frame.hidden_constraint.Size() != hidden_constraint_count_) ||
frame.angular_velocity.Size() != node_count * kAngularDofsPerNode ||
frame.angular_acceleration.Size() != node_count * kAngularDofsPerNode ||
!IsFinite(frame.angular_velocity) ||
!IsFinite(frame.angular_acceleration) ||
!std::isfinite(frame.kinetic_energy) ||
!std::isfinite(frame.physical_strain_energy) ||
!std::isfinite(frame.drilling_energy) ||
!std::isfinite(frame.external_work) ||
!std::isfinite(frame.energy_defect) ||
!IsFinite(frame.global_equilibrium) || !IsFinite(frame.linear_momentum) ||
!IsFinite(frame.angular_momentum) ||
!IsFinite(frame.linear_momentum_rate) ||
!IsFinite(frame.angular_momentum_rate) ||
!IsFinite(frame.free_residual_metrics)) {
return Failure(FailureCategory::kSolver,
"Frame physical evidence is nonfinite or has invalid size.");
}
for (const std::size_t dof : constrained_dofs_) {
if (frame.generalized_coordinates[dof] != 0.0 ||
frame.velocity[dof] != 0.0 || frame.acceleration[dof] != 0.0 ||
frame.algorithmic_acceleration[dof] != 0.0) {
return Failure(FailureCategory::kSolver,
"Fixed boundary components must remain exactly zero.");
}
}
for (const Vector* scales :
{&frame.hidden_constraint_scale, &frame.hidden_velocity_defect_scale,
&frame.hidden_acceleration_defect_scale}) {
for (std::size_t index = 0U; index < scales->Size(); ++index) {
if ((*scales)[index] < 0.0) {
return Failure(FailureCategory::kSolver,
"Hidden evidence scales must be nonnegative.");
}
}
}
if (frame.free_residual_metrics[0U] < 0.0 ||
frame.free_residual_metrics[1U] < 0.0 ||
(frame.is_initial &&
(frame.external_work != 0.0 || frame.energy_defect != 0.0))) {
return Failure(FailureCategory::kSolver,
"Frame metric or initial energy evidence is invalid.");
}
std::size_t constraint = 0U;
for (std::size_t dof = 0U; dof < full_dof_count_; ++dof) {
const bool constrained = constraint < constrained_dofs_.size() &&
constrained_dofs_[constraint] == dof;
if (constrained) ++constraint;
if (frame.residual[dof] != frame.inertia_force[dof] +
frame.internal_force[dof] -
frame.external_force[dof] ||
frame.reaction[dof] != (constrained ? frame.residual[dof] : 0.0) ||
frame.residual_scale[dof] < 0.0 ||
(!accepted_frames_.empty() &&
frame.external_force[dof] !=
accepted_frames_.front().external_force[dof]) ||
(frame.is_initial &&
(frame.generalized_coordinates[dof] != 0.0 ||
frame.velocity[dof] != 0.0 ||
frame.acceleration[dof] != frame.algorithmic_acceleration[dof]))) {
return Failure(
FailureCategory::kSolver,
"Frame force, reaction, or initial-state identity is invalid.");
}
}
const Status shell_status = ValidateShellRows(frame, shell_element_order_);
if (!shell_status.IsOk()) {
return shell_status;
}
const std::size_t hidden_constraint_count = frame.hidden_constraint.Size();
// Copy insertion preserves the previous vector when allocation/copy fails.
accepted_frames_.push_back(frame);
hidden_constraint_count_ = hidden_constraint_count;
return Status::Ok();
}
void DynamicAnalysisState::DiscardTrial() noexcept {}
const std::vector<DynamicAcceptedFrameState>&
DynamicAnalysisState::AcceptedFrames() const noexcept {
return accepted_frames_;
}
const DynamicAcceptedFrameState& DynamicAnalysisState::CommittedState() const {
if (accepted_frames_.empty()) {
throw std::logic_error{
"Committed dynamic state is unavailable before frame zero."};
}
return accepted_frames_.back();
}
bool DynamicAnalysisState::IsInitialized() const noexcept {
return initialized_;
}
DynamicAnalysisState::DynamicAnalysisState(
const std::size_t full_dof_count, const std::uint32_t increment_count,
const double initial_increment,
std::vector<EntityIndex> shell_element_order,
std::vector<std::size_t> constrained_dofs,
std::vector<SourceEntityId> nodal_source_ids,
std::vector<ShellNodeInitialFrame> initial_frames)
: full_dof_count_{full_dof_count},
increment_count_{increment_count},
initial_increment_{initial_increment},
shell_element_order_{std::move(shell_element_order)},
constrained_dofs_{std::move(constrained_dofs)},
nodal_source_ids_{std::move(nodal_source_ids)},
initial_frames_{std::move(initial_frames)},
initialized_{true} {}
} // namespace fesa