feat: implement consistent-mass HHT shell dynamics
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user