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
+732
View File
@@ -0,0 +1,732 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <iomanip>
#include <limits>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/math/matrix.h"
#include "fesa/solvers/linear/linear_solver.h"
#include "fesa/solvers/linear/tangent_solver.h"
namespace fesa {
namespace hht_internal {
namespace {
/// @brief Attaches a fail-closed dynamic diagnostic to the source model.
Status Failure(const Domain& domain, const std::string& code,
const std::string& message) {
return Status::Failure(FailureCategory::kSolver, {{Severity::kError,
code,
{domain.SourcePath(), 0U},
"*DYNAMIC",
{},
message}});
}
/// @brief Reads one structural entry, including an implicit exact zero.
double Entry(const SparseMatrix& matrix, const std::size_t row,
const std::size_t column) {
const auto first = matrix.ColumnIndices().begin() +
static_cast<std::ptrdiff_t>(matrix.RowOffsets()[row]);
const auto last = matrix.ColumnIndices().begin() +
static_cast<std::ptrdiff_t>(matrix.RowOffsets()[row + 1U]);
const auto found = std::lower_bound(first, last, column);
return found != last && *found == column
? matrix.Values()[static_cast<std::size_t>(
found - matrix.ColumnIndices().begin())]
: 0.0;
}
/// @brief Applies the fixed translation length scale by full-space row.
double Scale(const std::size_t full_dof, const double length) {
return full_dof % 6U < 3U ? length : 1.0;
}
/// @brief Checks a dimensionless ratio without inventing a zero-scale floor.
bool Within(const double value, const double scale, const double tolerance) {
return std::isfinite(value) && std::isfinite(scale) && scale >= 0.0 &&
(scale == 0.0 ? value == 0.0 : std::abs(value) / scale <= tolerance);
}
/// @brief Verifies dimensions and finite values before algebraic contractions.
Status Validate(const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly, const double length) {
const std::size_t count = dofs.FullDofCount();
if (!std::isfinite(length) || !(length > 0.0) || count % 6U != 0U ||
assembly.rotations.size() != count / 6U) {
return Failure(domain, "dynamic-invalid-algebra-state",
"Dynamic algebra requires complete rotations and a positive "
"reference length.");
}
for (const Vector* vector :
{&assembly.frame.velocity, &assembly.frame.acceleration,
&assembly.frame.internal_force, &assembly.frame.external_force,
&assembly.frame.inertia_force, &assembly.frame.residual_scale,
&assembly.force_directional_second,
&assembly.force_directional_second_scale}) {
if (vector->Size() != count) {
return Failure(
domain, "dynamic-invalid-algebra-state",
"Dynamic algebra vectors must match the full equation space.");
}
for (std::size_t i = 0; i < count; ++i) {
if (!std::isfinite((*vector)[i])) {
return Failure(domain, "dynamic-invalid-algebra-state",
"Dynamic algebra vectors must remain finite.");
}
}
}
for (const Vector* vector :
{&assembly.hidden_second, &assembly.hidden_second_scale}) {
if (vector->Size() != assembly.rotations.size()) {
return Failure(
domain, "dynamic-invalid-algebra-state",
"Exact scalar hidden Hessian evidence must cover every node.");
}
for (std::size_t i = 0; i < vector->Size(); ++i) {
if (!std::isfinite((*vector)[i]) ||
(vector == &assembly.hidden_second_scale && (*vector)[i] < 0.0)) {
return Failure(domain, "dynamic-invalid-algebra-state",
"Scalar hidden Hessian evidence must be finite with "
"nonnegative scale.");
}
}
}
for (const SparseMatrix* matrix : {&assembly.mass, &assembly.internal_tangent,
&assembly.drilling_metric}) {
if (matrix->Rows() != count || matrix->Columns() != count ||
!matrix->Validate().IsOk()) {
return Failure(
domain, "dynamic-invalid-algebra-state",
"Dynamic algebra operators must match the full equation space.");
}
}
for (const auto& rotation : assembly.rotations) {
if (!rotation.drill_null.IsFinite() ||
!(std::abs(rotation.drill_null.Dot(rotation.drill_null) - 1.0) <=
1.0e-12)) {
return Failure(
domain, "dynamic-invalid-analytic-null",
"Every analytic drilling null must remain finite and unit length.");
}
}
return Status::Ok();
}
using BasisRow = std::vector<std::pair<std::size_t, double>>;
/// @brief Holds sparse nodal physical-complement columns and their mass.
struct PhysicalSpace {
std::vector<BasisRow> rows;
SparseMatrix mass;
};
/// @brief Checks a nonempty nodal physical block without removing small pivots.
Status CheckLocalMass(const Domain& domain, const SparseMatrix& matrix,
const std::size_t begin, const std::size_t end) {
if (begin == end) return Status::Ok();
const std::size_t size = end - begin;
std::array<std::array<double, 6>, 6> block{};
std::array<std::array<double, 6>, 6> lower{};
double scale = 0.0;
for (std::size_t i = 0; i < size; ++i) {
for (std::size_t j = 0; j < size; ++j) {
block[i][j] = Entry(matrix, begin + i, begin + j);
scale = std::max(scale, std::abs(block[i][j]));
}
}
if (!std::isfinite(scale) || !(scale > 0.0)) {
return Failure(domain, "dynamic-positive-mass",
"A nonempty physical mass block has no positive scale.");
}
double norm = 0.0;
for (std::size_t i = 0; i < size; ++i) {
double sum = 0.0;
for (std::size_t j = 0; j < size; ++j) {
block[i][j] /= scale;
sum += std::abs(block[i][j]);
}
norm = std::max(norm, sum);
for (std::size_t j = 0; j <= i; ++j) {
double value = block[i][j];
for (std::size_t k = 0; k < j; ++k) value -= lower[i][k] * lower[j][k];
if (!std::isfinite(value) || (i == j && !(value > 0.0))) {
return Failure(
domain, "dynamic-positive-mass",
"Physical mass requires strictly positive Cholesky pivots.");
}
lower[i][j] = i == j ? std::sqrt(value) : value / lower[j][j];
}
}
std::array<double, 6> sums{};
for (std::size_t column = 0; column < size; ++column) {
std::array<double, 6> solution{};
for (std::size_t i = 0; i < size; ++i) {
double value = i == column ? 1.0 : 0.0;
for (std::size_t k = 0; k < i; ++k) value -= lower[i][k] * solution[k];
solution[i] = value / lower[i][i];
}
for (std::size_t reverse = size; reverse > 0U; --reverse) {
const std::size_t i = reverse - 1U;
double value = solution[i];
for (std::size_t k = i + 1U; k < size; ++k)
value -= lower[k][i] * solution[k];
solution[i] = value / lower[i][i];
sums[i] += std::abs(solution[i]);
}
}
const double inverse_norm = *std::max_element(sums.begin(), sums.end());
const double rcond = 1.0 / norm / inverse_norm;
if (!std::isfinite(inverse_norm) || !std::isfinite(rcond) ||
!(rcond >= std::ldexp(1.0, -32))) {
return Failure(
domain, "dynamic-positive-mass-conditioning",
"A nodal free physical block reciprocal condition is below 2^-32.");
}
return Status::Ok();
}
/// @brief Builds the actual free-space analytic complement, without rank cuts.
Result<PhysicalSpace> BuildPhysicalSpace(
const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly, const double length,
const std::vector<std::size_t>& null_nodes, LinearSolver& solver) {
const auto scaled = ScaledFree(assembly.mass, dofs, length);
if (!scaled.HasValue())
return Result<PhysicalSpace>::Failure(scaled.GetStatus());
const SparseMatrix& mass = scaled.Value();
double mass_scale = 0.0;
for (const double entry : mass.Values())
mass_scale = std::max(mass_scale, std::abs(entry));
const std::size_t no_null = std::numeric_limits<std::size_t>::max();
std::vector<std::size_t> node_null(assembly.rotations.size(), no_null);
for (std::size_t i = 0; i < null_nodes.size(); ++i)
node_null[null_nodes[i]] = i;
double mass_norm = 0.0;
double symmetry_error = 0.0;
double null_error = 0.0;
if (mass_scale > 0.0) {
for (std::size_t row = 0; row < mass.Rows(); ++row) {
std::vector<double> action(null_nodes.size(), 0.0);
for (std::size_t index = mass.RowOffsets()[row];
index < mass.RowOffsets()[row + 1U]; ++index) {
const std::size_t column = mass.ColumnIndices()[index];
const double entry = mass.Values()[index] / mass_scale;
mass_norm = std::hypot(mass_norm, entry);
symmetry_error = std::hypot(
symmetry_error, entry - Entry(mass, column, row) / mass_scale);
const std::size_t full = dofs.FreeDofs()[column];
if (full % 6U >= 3U && node_null[full / 6U] != no_null) {
action[node_null[full / 6U]] +=
entry * assembly.rotations[full / 6U].drill_null[full % 6U - 3U];
}
}
for (const double value : action)
null_error = std::hypot(null_error, value);
}
}
if ((mass_norm > 0.0 && symmetry_error / mass_norm > 1.0e-12) ||
(!null_nodes.empty() && mass_norm > 0.0 &&
null_error / mass_norm /
std::sqrt(static_cast<double>(null_nodes.size())) >
1.0e-12)) {
return Result<PhysicalSpace>::Failure(
Failure(domain, "dynamic-invalid-mass-null",
"Scaled free mass symmetry or analytic null action failed."));
}
std::vector<BasisRow> rows(dofs.FullDofCount());
std::vector<std::size_t> node_starts;
std::size_t dimension = 0U;
for (std::size_t node = 0; node < assembly.rotations.size(); ++node) {
node_starts.push_back(dimension);
for (std::size_t component = 0; component < 3; ++component) {
if (dofs.FreeEquation(6U * node + component).has_value()) {
rows[6U * node + component].push_back({dimension++, 1.0});
}
}
std::vector<std::size_t> free_rotations;
for (std::size_t component = 0; component < 3; ++component) {
if (dofs.FreeEquation(6U * node + 3U + component).has_value()) {
free_rotations.push_back(component);
}
}
if (node_null[node] == no_null) {
for (const std::size_t component : free_rotations) {
rows[6U * node + 3U + component].push_back({dimension++, 1.0});
}
continue;
}
const Vector3 n = assembly.rotations[node].drill_null;
std::stable_sort(free_rotations.begin(), free_rotations.end(),
[&](const std::size_t a, const std::size_t b) {
return std::abs(n[a]) < std::abs(n[b]);
});
std::vector<Vector3> basis;
for (std::size_t column = 0; column + 1U < free_rotations.size();
++column) {
std::array<double, 3> axis{};
axis[free_rotations[column]] = 1.0;
Vector3 tangent = Vector3{axis} - n[free_rotations[column]] * n;
for (const Vector3& previous : basis)
tangent = tangent - tangent.Dot(previous) * previous;
const double norm = tangent.Norm();
if (!std::isfinite(norm) || !(norm > 0.0)) {
return Result<PhysicalSpace>::Failure(Failure(
domain, "dynamic-invalid-analytic-null",
"The free physical complement lost its analytic dimension."));
}
tangent = tangent / norm;
basis.push_back(tangent);
for (const std::size_t component : free_rotations) {
rows[6U * node + 3U + component].push_back(
{dimension, tangent[component]});
}
++dimension;
}
}
std::vector<CooContribution> entries;
node_starts.push_back(dimension);
double complement_gram_error = 0.0;
double null_gram_error = 0.0;
double cross_gram_error = 0.0;
for (std::size_t node = 0; node < assembly.rotations.size(); ++node) {
const std::size_t first = node_starts[node];
const std::size_t last = node_starts[node + 1U];
std::size_t free_count = 0U;
for (std::size_t c = 0; c < 6; ++c) {
if (dofs.FreeEquation(6U * node + c).has_value()) ++free_count;
}
const bool has_null = node_null[node] != no_null;
if (last - first + (has_null ? 1U : 0U) != free_count) {
return Result<PhysicalSpace>::Failure(Failure(
domain, "dynamic-invalid-analytic-null",
"The free physical and null bases do not span the free space."));
}
for (std::size_t a = first; a < last; ++a) {
for (std::size_t b = first; b < last; ++b) {
double gram = a == b ? -1.0 : 0.0;
for (std::size_t c = 0; c < 6; ++c) {
double pa = 0.0;
double pb = 0.0;
for (const auto& entry : rows[6U * node + c]) {
if (entry.first == a) pa = entry.second;
if (entry.first == b) pb = entry.second;
}
gram += pa * pb;
}
complement_gram_error = std::hypot(complement_gram_error, gram);
}
if (has_null) {
double gram = 0.0;
for (std::size_t c = 0; c < 3; ++c) {
for (const auto& entry : rows[6U * node + 3U + c]) {
if (entry.first == a)
gram += entry.second * assembly.rotations[node].drill_null[c];
}
}
cross_gram_error = std::hypot(cross_gram_error, gram);
}
}
if (has_null)
null_gram_error =
std::hypot(null_gram_error, assembly.rotations[node].drill_null.Dot(
assembly.rotations[node].drill_null) -
1.0);
}
const double full_gram_error = std::hypot(
complement_gram_error,
std::hypot(null_gram_error, std::sqrt(2.0) * cross_gram_error));
if (!(cross_gram_error <= 1.0e-12) || !(full_gram_error <= 1.0e-12) ||
(!null_nodes.empty() &&
!(null_gram_error / std::sqrt(static_cast<double>(null_nodes.size())) <=
1.0e-12))) {
return Result<PhysicalSpace>::Failure(Failure(
domain, "dynamic-invalid-analytic-null",
"The scaled physical and analytic null Gram identities failed."));
}
for (std::size_t i = 0; i < dimension; ++i)
entries.push_back({i, i, 0.0, 0U, entries.size()});
for (std::size_t row = 0; row < mass.Rows(); ++row) {
const auto& left = rows[dofs.FreeDofs()[row]];
for (std::size_t index = mass.RowOffsets()[row];
index < mass.RowOffsets()[row + 1U]; ++index) {
const auto& right = rows[dofs.FreeDofs()[mass.ColumnIndices()[index]]];
for (const auto& a : left) {
for (const auto& b : right) {
entries.push_back({a.first, b.first,
a.second * mass.Values()[index] * b.second, 0U,
entries.size()});
}
}
}
}
auto block = MakeSparse(dimension, dimension, std::move(entries));
if (!block.HasValue())
return Result<PhysicalSpace>::Failure(block.GetStatus());
for (std::size_t node = 0; node < assembly.rotations.size(); ++node) {
const Status local = CheckLocalMass(
domain, block.Value(), node_starts[node], node_starts[node + 1U]);
if (!local.IsOk()) return Result<PhysicalSpace>::Failure(local);
}
if (dimension > 0U) {
const Status factor = solver.Factorize(block.Value());
if (!factor.IsOk()) return Result<PhysicalSpace>::Failure(factor);
}
return Result<PhysicalSpace>::Success(
{std::move(rows), std::move(block.Value())});
}
/// @brief Builds psi_q from the current unit-null derivatives and force
/// Hessian.
Result<SparseMatrix> ConstraintGradient(const DynamicAssembly& assembly,
const std::vector<std::size_t>& nodes) {
std::vector<CooContribution> entries;
const auto& stiffness = assembly.internal_tangent;
for (std::size_t row = 0; row < nodes.size(); ++row) {
const std::size_t node = nodes[row];
const auto& rotation = assembly.rotations[node];
for (std::size_t c = 0; c < 3; ++c) {
const std::size_t full = 6U * node + 3U + c;
const double g = assembly.frame.internal_force[full] -
assembly.frame.external_force[full];
for (std::size_t d = 0; d < 3; ++d) {
entries.push_back({row, 6U * node + 3U + d,
rotation.null_gradient(c, d) * g, 0U,
entries.size()});
}
for (std::size_t index = stiffness.RowOffsets()[full];
index < stiffness.RowOffsets()[full + 1U]; ++index) {
entries.push_back({row, stiffness.ColumnIndices()[index],
rotation.drill_null[c] * stiffness.Values()[index],
0U, entries.size()});
}
}
}
return MakeSparse(nodes.size(), stiffness.Columns(), std::move(entries));
}
/// @brief Stores W-scaled regularity with exact inverse-action infinity norms.
struct Regularity {
SparseMatrix matrix;
Vector weights;
};
/// @brief Factors W^-1 psi_q N W^-1 and checks the prescribed condition bound.
Result<Regularity> CheckRegularity(const Domain& domain,
const DynamicAssembly& assembly,
const std::vector<std::size_t>& nodes,
const SparseMatrix& gradient,
TangentSolver& solver) {
const std::size_t no_null = std::numeric_limits<std::size_t>::max();
std::vector<std::size_t> node_null(assembly.rotations.size(), no_null);
Vector weights{nodes.size()};
for (std::size_t i = 0; i < nodes.size(); ++i) {
node_null[nodes[i]] = i;
const auto& n = assembly.rotations[nodes[i]].drill_null;
double stiffness = 0.0;
for (std::size_t a = 0; a < 3; ++a) {
for (std::size_t b = 0; b < 3; ++b) {
stiffness += n[a] *
Entry(assembly.drilling_metric, 6U * nodes[i] + 3U + a,
6U * nodes[i] + 3U + b) *
n[b];
}
}
if (!std::isfinite(stiffness) || !(stiffness > 0.0)) {
return Result<Regularity>::Failure(
Failure(domain, "dynamic-algebraic-regularity",
"Every admissible drill null requires positive finite "
"drilling metric."));
}
weights[i] = std::sqrt(stiffness);
}
std::vector<CooContribution> entries;
for (std::size_t row = 0; row < nodes.size(); ++row) {
entries.push_back({row, row, 0.0, 0U, entries.size()});
for (std::size_t index = gradient.RowOffsets()[row];
index < gradient.RowOffsets()[row + 1U]; ++index) {
const std::size_t full = gradient.ColumnIndices()[index];
if (full % 6U >= 3U && node_null[full / 6U] != no_null) {
const std::size_t column = node_null[full / 6U];
entries.push_back(
{row, column,
gradient.Values()[index] *
assembly.rotations[full / 6U].drill_null[full % 6U - 3U] /
weights[row] / weights[column],
0U, entries.size()});
}
}
}
auto matrix = MakeSparse(nodes.size(), nodes.size(), std::move(entries));
if (!matrix.HasValue())
return Result<Regularity>::Failure(matrix.GetStatus());
if (nodes.empty())
return Result<Regularity>::Success(
{std::move(matrix.Value()), std::move(weights)});
const Status factor = solver.FactorizeFresh(matrix.Value());
if (!factor.IsOk()) return Result<Regularity>::Failure(factor);
double norm = 0.0;
for (std::size_t row = 0; row < nodes.size(); ++row) {
double sum = 0.0;
for (std::size_t index = matrix.Value().RowOffsets()[row];
index < matrix.Value().RowOffsets()[row + 1U]; ++index)
sum += std::abs(matrix.Value().Values()[index]);
norm = std::max(norm, sum);
}
Vector inverse_sums{nodes.size()};
// Columns are discarded after their absolute row sums are accumulated;
// a global dense inverse is never retained.
for (std::size_t column = 0; column < nodes.size(); ++column) {
Vector rhs{nodes.size()};
rhs[column] = 1.0;
Vector solution{nodes.size()};
const Status solved = solver.SolveCorrection(rhs, solution);
if (!solved.IsOk()) return Result<Regularity>::Failure(solved);
const Status checked = CheckLinearSolve(matrix.Value(), rhs, solution);
if (!checked.IsOk()) return Result<Regularity>::Failure(checked);
for (std::size_t row = 0; row < nodes.size(); ++row)
inverse_sums[row] += std::abs(solution[row]);
}
double inverse_norm = 0.0;
for (std::size_t row = 0; row < nodes.size(); ++row)
inverse_norm = std::max(inverse_norm, inverse_sums[row]);
const double condition = 1.0 / norm / inverse_norm;
if (!std::isfinite(norm) || !std::isfinite(inverse_norm) ||
!std::isfinite(condition) || !(condition >= std::ldexp(1.0, -32))) {
std::ostringstream message;
message << std::setprecision(17)
<< "Scaled null regularity reciprocal infinity condition "
<< condition << " is below 2^-32; norm=" << norm
<< ", inverse-action norm=" << inverse_norm << ".";
return Result<Regularity>::Failure(
Failure(domain, "dynamic-algebraic-conditioning", message.str()));
}
return Result<Regularity>::Success(
{std::move(matrix.Value()), std::move(weights)});
}
/// @brief Recovers raw hidden equations and uncancelled derivative term scales.
Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
const std::vector<std::size_t>& nodes,
const SparseMatrix& gradient,
DynamicAcceptedFrameState* frame,
const bool require_acceleration) {
Vector psi{nodes.size()};
Vector psi_scale{nodes.size()};
Vector velocity_defect = gradient.Multiply(frame->velocity);
Vector acceleration_defect = gradient.Multiply(frame->acceleration);
Vector velocity_scale{nodes.size()};
Vector acceleration_scale{nodes.size()};
for (std::size_t row = 0; row < nodes.size(); ++row) {
const std::size_t node = nodes[row];
const auto& rotation = assembly.rotations[node];
for (std::size_t index = gradient.RowOffsets()[row];
index < gradient.RowOffsets()[row + 1U]; ++index) {
const std::size_t column = gradient.ColumnIndices()[index];
velocity_scale[row] +=
std::abs(gradient.Values()[index] * frame->velocity[column]);
acceleration_scale[row] +=
std::abs(gradient.Values()[index] * frame->acceleration[column]);
}
for (std::size_t c = 0; c < 3; ++c) {
const std::size_t full = 6U * node + 3U + c;
const double g =
frame->internal_force[full] - frame->external_force[full];
const double n = rotation.drill_null[c];
psi[row] += n * g;
psi_scale[row] += std::abs(n) * frame->residual_scale[full];
}
acceleration_defect[row] += assembly.hidden_second[node];
acceleration_scale[row] += assembly.hidden_second_scale[node];
if (!Within(psi[row], psi_scale[row], 1.0e-10) ||
(frame->is_initial &&
!Within(velocity_defect[row], velocity_scale[row], 1.0e-10)) ||
(frame->is_initial && require_acceleration &&
!Within(acceleration_defect[row], acceleration_scale[row], 1.0e-10)) ||
!std::isfinite(velocity_defect[row]) ||
!std::isfinite(acceleration_defect[row]) ||
!std::isfinite(velocity_scale[row]) ||
!std::isfinite(acceleration_scale[row])) {
std::ostringstream message;
message << std::setprecision(17)
<< "Dynamic null consistency failed at node "
<< domain.Nodes()[node].source_id.instance_name << ":"
<< domain.Nodes()[node].source_id.source_label_text
<< "; psi=" << psi[row] << ", psi_scale=" << psi_scale[row]
<< ", velocity=" << velocity_defect[row]
<< ", velocity_scale=" << velocity_scale[row]
<< ", acceleration=" << acceleration_defect[row]
<< ", acceleration_scale=" << acceleration_scale[row]
<< ", initial=" << frame->is_initial
<< ", enforce_acceleration=" << require_acceleration << ".";
return Failure(domain, "dynamic-hidden-constraint", message.str());
}
}
frame->hidden_constraint = std::move(psi);
frame->hidden_constraint_scale = std::move(psi_scale);
frame->hidden_velocity_defect = std::move(velocity_defect);
frame->hidden_velocity_defect_scale = std::move(velocity_scale);
frame->hidden_acceleration_defect = std::move(acceleration_defect);
frame->hidden_acceleration_defect_scale = std::move(acceleration_scale);
return Status::Ok();
}
} // namespace
std::vector<std::size_t> NullNodes(
const DofManager& dofs,
const std::vector<DynamicRotationKinematics>& rotations) {
std::vector<std::size_t> nodes;
for (std::size_t node = 0; node < rotations.size(); ++node) {
bool admissible = true;
for (std::size_t component = 0; component < 3; ++component) {
if (!dofs.FreeEquation(6U * node + 3U + component).has_value() &&
rotations[node].drill_null[component] != 0.0)
admissible = false;
}
if (admissible) nodes.push_back(node);
}
return nodes;
}
Result<Vector> InitializeAcceleration(const Domain& domain,
const DofManager& dofs,
const DynamicAssembly& initial,
const double length,
LinearSolver& positive_solver,
TangentSolver& tangent_solver) {
const Status valid = Validate(domain, dofs, initial, length);
if (!valid.IsOk()) return Result<Vector>::Failure(valid);
const auto nodes = NullNodes(dofs, initial.rotations);
auto physical =
BuildPhysicalSpace(domain, dofs, initial, length, nodes, positive_solver);
if (!physical.HasValue())
return Result<Vector>::Failure(physical.GetStatus());
auto gradient = ConstraintGradient(initial, nodes);
if (!gradient.HasValue())
return Result<Vector>::Failure(gradient.GetStatus());
auto regularity =
CheckRegularity(domain, initial, nodes, gradient.Value(), tangent_solver);
if (!regularity.HasValue())
return Result<Vector>::Failure(regularity.GetStatus());
DynamicAcceptedFrameState candidate = initial.frame;
candidate.is_initial = true;
candidate.acceleration = Vector{dofs.FullDofCount()};
const Status consistent = HiddenEvidence(domain, initial, nodes,
gradient.Value(), &candidate, false);
if (!consistent.IsOk()) return Result<Vector>::Failure(consistent);
Vector rhs{physical.Value().mass.Rows()};
// The initialization evaluation has zero acceleration, so I(q,v,0)=c(q,v).
for (std::size_t full = 0; full < dofs.FullDofCount(); ++full) {
if (initial.frame.acceleration[full] != 0.0) {
return Result<Vector>::Failure(Failure(
domain, "dynamic-invalid-initial-acceleration",
"Initial inertia must be evaluated at zero unknown acceleration."));
}
const double force = initial.frame.external_force[full] -
initial.frame.internal_force[full] -
initial.frame.inertia_force[full];
for (const auto& entry : physical.Value().rows[full]) {
rhs[entry.first] += entry.second * Scale(full, length) * force;
}
}
Vector solution{rhs.Size()};
if (rhs.Size() > 0U) {
const Status solved = positive_solver.Solve(rhs, solution);
if (!solved.IsOk()) return Result<Vector>::Failure(solved);
const Status checked =
CheckLinearSolve(physical.Value().mass, rhs, solution);
if (!checked.IsOk()) return Result<Vector>::Failure(checked);
}
for (std::size_t full = 0; full < dofs.FullDofCount(); ++full) {
for (const auto& entry : physical.Value().rows[full]) {
candidate.acceleration[full] +=
Scale(full, length) * entry.second * solution[entry.first];
}
}
const Status physical_hidden = HiddenEvidence(
domain, initial, nodes, gradient.Value(), &candidate, false);
if (!physical_hidden.IsOk()) return Result<Vector>::Failure(physical_hidden);
if (!nodes.empty()) {
Vector null_rhs{nodes.size()};
for (std::size_t row = 0; row < nodes.size(); ++row) {
null_rhs[row] = -candidate.hidden_acceleration_defect[row] /
regularity.Value().weights[row];
}
Vector correction{nodes.size()};
const Status solved = tangent_solver.SolveCorrection(null_rhs, correction);
if (!solved.IsOk()) return Result<Vector>::Failure(solved);
const Status checked =
CheckLinearSolve(regularity.Value().matrix, null_rhs, correction);
if (!checked.IsOk()) return Result<Vector>::Failure(checked);
for (std::size_t i = 0; i < nodes.size(); ++i) {
for (std::size_t c = 0; c < 3; ++c) {
candidate.acceleration[6U * nodes[i] + 3U + c] +=
initial.rotations[nodes[i]].drill_null[c] * correction[i] /
regularity.Value().weights[i];
}
}
}
const Status hidden = HiddenEvidence(domain, initial, nodes, gradient.Value(),
&candidate, true);
if (!hidden.IsOk()) return Result<Vector>::Failure(hidden);
const Vector ma = initial.mass.Multiply(candidate.acceleration);
for (const std::size_t full : dofs.FreeDofs()) {
const double residual = ma[full] + initial.frame.inertia_force[full] +
initial.frame.internal_force[full] -
initial.frame.external_force[full];
double scale = std::abs(initial.frame.inertia_force[full]) +
std::abs(initial.frame.internal_force[full]) +
std::abs(initial.frame.external_force[full]);
for (std::size_t index = initial.mass.RowOffsets()[full];
index < initial.mass.RowOffsets()[full + 1U]; ++index) {
scale +=
std::abs(initial.mass.Values()[index] *
candidate.acceleration[initial.mass.ColumnIndices()[index]]);
}
if (!Within(residual, scale, 1.0e-10)) {
return Result<Vector>::Failure(Failure(
domain, "dynamic-initial-equilibrium",
"Initial physical acceleration failed the complete free equation."));
}
}
return Result<Vector>::Success(std::move(candidate.acceleration));
}
Status CompleteConstraintEvidence(
const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly, const double length,
const std::vector<std::size_t>& initial_null_nodes,
LinearSolver& positive_solver, TangentSolver& tangent_solver,
DynamicAcceptedFrameState* frame) {
const Status valid = Validate(domain, dofs, assembly, length);
if (!valid.IsOk()) return valid;
const auto nodes = NullNodes(dofs, assembly.rotations);
if (nodes != initial_null_nodes) {
return Failure(domain, "dynamic-mass-null-rank-change",
"The admissible analytic drilling-null inventory changed.");
}
const auto physical = BuildPhysicalSpace(domain, dofs, assembly, length,
nodes, positive_solver);
if (!physical.HasValue()) return physical.GetStatus();
const auto gradient = ConstraintGradient(assembly, nodes);
if (!gradient.HasValue()) return gradient.GetStatus();
const auto regularity = CheckRegularity(domain, assembly, nodes,
gradient.Value(), tangent_solver);
if (!regularity.HasValue()) return regularity.GetStatus();
DynamicAcceptedFrameState candidate = *frame;
const Status hidden = HiddenEvidence(domain, assembly, nodes,
gradient.Value(), &candidate, true);
if (!hidden.IsOk()) return hidden;
*frame = std::move(candidate);
return Status::Ok();
}
} // namespace hht_internal
} // namespace fesa
+483
View File
@@ -0,0 +1,483 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/finite_rotation.h"
#include "fesa/loads/load.h"
#include "fesa/model/source_target_resolver.h"
namespace fesa::hht_internal {
namespace {
/// @brief Reports a dynamic assembly or equilibrium failure.
Status Failure(const std::string& message) {
return Status::Failure(FailureCategory::kSolver, {{Severity::kError,
"dynamic-invalid-state",
{},
"DYNAMIC",
{},
message}});
}
/// @brief Uses exact zero semantics for a dimensionally consistent ratio.
double Ratio(const double numerator, const double denominator) {
return denominator > 0.0 ? numerator / denominator
: numerator == 0.0 ? 0.0
: (std::numeric_limits<double>::infinity)();
}
/// @brief Gathers one source-node Cartesian component triple.
Vector3 Triple(const Vector& vector, const std::size_t first) {
return {vector[first], vector[first + 1U], vector[first + 2U]};
}
/// @brief Computes one matrix-vector spatial transform.
Vector3 Transform(const Matrix& matrix, const Vector3& vector) {
return {matrix(0U, 0U) * vector[0U] + matrix(0U, 1U) * vector[1U] +
matrix(0U, 2U) * vector[2U],
matrix(1U, 0U) * vector[0U] + matrix(1U, 1U) * vector[1U] +
matrix(1U, 2U) * vector[2U],
matrix(2U, 0U) * vector[0U] + matrix(2U, 1U) * vector[1U] +
matrix(2U, 2U) * vector[2U]};
}
/// @brief Gathers a local 24-vector in the DOF-owner scatter order.
Vector Gather(const Vector& full, const RuntimeShell& shell) {
Vector local(24U);
for (std::size_t i = 0U; i < 24U; ++i) local[i] = full[shell.scatter[i]];
return local;
}
} // namespace
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
const DofManager& dofs) {
std::vector<RuntimeShell> shells;
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
const auto* definition =
dynamic_cast<const Mitc4ShellDefinition*>(&domain.Elements()[index]);
if (definition == nullptr ||
definition->section_index >= domain.Properties().Size() ||
definition->material_index >= domain.Materials().Size()) {
return Result<std::vector<RuntimeShell>>::Failure(
Failure("HHT requires valid MITC4 shell definitions."));
}
const auto* section = dynamic_cast<const ShellSection*>(
&domain.Properties()[definition->section_index]);
const auto* material = dynamic_cast<const LinearElasticMaterial*>(
&domain.Materials()[definition->material_index]);
if (section == nullptr || material == nullptr) {
return Result<std::vector<RuntimeShell>>::Failure(
Failure("HHT requires homogeneous elastic shell properties."));
}
double density = 0.0;
for (const auto& record : domain.Densities()) {
if (record.material_index == definition->material_index)
density = record.rho;
}
std::array<const Node*, 4> nodes{};
std::array<ShellNodeInitialFrame, 4> frames{};
for (std::size_t local = 0U; local < 4U; ++local) {
const auto node = definition->node_indices[local];
if (node >= domain.Nodes().size() ||
node >= domain.ShellNodeInitialFrames().size()) {
return Result<std::vector<RuntimeShell>>::Failure(
Failure("A dynamic shell node has no initial frame."));
}
nodes[local] = &domain.Nodes()[node];
frames[local] = domain.ShellNodeInitialFrames()[node];
}
auto internal =
NonlinearMitc4Shell::Create(nodes, frames, *section, *material);
if (!internal.HasValue())
return Result<std::vector<RuntimeShell>>::Failure(internal.GetStatus());
auto inertia = NonlinearMitc4Inertia::Create(nodes, frames, *section,
*material, density);
if (!inertia.HasValue())
return Result<std::vector<RuntimeShell>>::Failure(inertia.GetStatus());
shells.push_back({static_cast<EntityIndex>(index),
dofs.ShellElementScatter(static_cast<EntityIndex>(index)),
std::move(internal.Value()), std::move(inertia.Value())});
}
return Result<std::vector<RuntimeShell>>::Success(std::move(shells));
}
Result<double> ReferenceLength(const Domain& domain) {
double length = 0.0;
for (std::size_t i = 0U; i < domain.ShellElements().Size(); ++i) {
const auto& element = domain.ShellElements()[i];
for (std::size_t node = 0U; node < 4U; ++node) {
const Vector3 a(domain.Nodes()[element.node_indices[node]].coordinates);
const Vector3 b(
domain.Nodes()[element.node_indices[(node + 1U) % 4U]].coordinates);
length = (std::max)(length, (a - b).Norm());
}
}
if (!std::isfinite(length) || length <= 0.0)
return Result<double>::Failure(
Failure("The dynamic reference length must be positive."));
return Result<double>::Success(length);
}
Result<std::array<Vector, 2>> AssembleLoads(const Domain& domain,
const DofManager& dofs) {
const auto index = SourceTargetIndex::FromDomain(domain);
const SourceTargetResolver resolver(index);
const LoadContext context{domain, dofs, resolver};
std::array<Vector, 2> result{Vector(dofs.FullDofCount()),
Vector(dofs.FullDofCount())};
for (const auto& load : domain.Steps()[0U].Loads()) {
const auto contributions = load.get().ComputeContributions(context);
if (!contributions.HasValue())
return Result<std::array<Vector, 2>>::Failure(contributions.GetStatus());
for (const auto& entry : contributions.Value()) {
result[0U][entry.full_dof_index] += entry.value;
result[1U][entry.full_dof_index] += std::abs(entry.value);
if (!std::isfinite(result[0U][entry.full_dof_index]) ||
!std::isfinite(result[1U][entry.full_dof_index]))
return Result<std::array<Vector, 2>>::Failure(
Failure("Dynamic load reduction overflowed."));
}
}
return Result<std::array<Vector, 2>>::Success(std::move(result));
}
Result<SparseMatrix> MakeSparse(const std::size_t rows,
const std::size_t columns,
std::vector<CooContribution> contributions) {
std::vector<std::vector<std::size_t>> row_columns(rows);
for (const auto& entry : contributions) {
if (entry.row >= rows || entry.column >= columns)
return Result<SparseMatrix>::Failure(
Failure("Projected sparse index is invalid."));
row_columns[entry.row].push_back(entry.column);
}
SparsePattern pattern;
pattern.row_offsets.push_back(0U);
for (auto& row : row_columns) {
std::sort(row.begin(), row.end());
row.erase(std::unique(row.begin(), row.end()), row.end());
pattern.column_indices.insert(pattern.column_indices.end(), row.begin(),
row.end());
pattern.row_offsets.push_back(pattern.column_indices.size());
}
return SparseMatrix::FromCoo(rows, columns, std::move(contributions),
pattern);
}
Result<SparseMatrix> ScaledFree(const SparseMatrix& matrix,
const DofManager& dofs, const double length) {
std::vector<CooContribution> entries;
for (std::size_t i = 0U; i < dofs.FreeDofCount(); ++i) {
const auto row = dofs.FreeDofs()[i];
for (auto offset = matrix.RowOffsets()[row];
offset < matrix.RowOffsets()[row + 1U]; ++offset) {
const auto column = matrix.ColumnIndices()[offset];
const auto free_column = dofs.FreeEquation(column);
if (free_column.has_value()) {
const double factor =
(row % 6U < 3U ? length : 1.0) * (column % 6U < 3U ? length : 1.0);
entries.push_back(
{i, *free_column, factor * matrix.Values()[offset], row, offset});
}
}
}
return MakeSparse(dofs.FreeDofCount(), dofs.FreeDofCount(),
std::move(entries));
}
Status CheckLinearSolve(const SparseMatrix& matrix, const Vector& rhs,
const Vector& solution) {
if (matrix.Rows() != rhs.Size() || matrix.Columns() != solution.Size())
return Failure("Dynamic linear solve dimensions differ.");
double numerator = 0.0;
double denominator = 0.0;
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
double residual = -rhs[row];
double scale = std::abs(rhs[row]);
for (auto offset = matrix.RowOffsets()[row];
offset < matrix.RowOffsets()[row + 1U]; ++offset) {
const double term =
matrix.Values()[offset] * solution[matrix.ColumnIndices()[offset]];
residual += term;
scale += std::abs(term);
}
if (!std::isfinite(residual) || !std::isfinite(scale))
return Failure("Dynamic linear solve returned nonfinite data.");
numerator = (std::max)(numerator, std::abs(residual));
denominator = (std::max)(denominator, scale);
}
if (Ratio(numerator, denominator) > 1e-12) {
return Status::Failure(FailureCategory::kSolver,
{{Severity::kError,
"dynamic-linear-solve",
{},
"DYNAMIC",
{},
"Dynamic linear backward error exceeds 1e-12."}});
}
return Status::Ok();
}
Result<DynamicAssembly> EvaluateAssembly(
const Domain& domain, const DofManager& dofs,
const std::vector<RuntimeShell>& shells, DynamicAcceptedFrameState frame,
const Vector& load_scale, const double time_increment,
const HhtParameters& parameters, const ParallelFor& parallel_for,
const bool directional_third) {
const std::size_t count = dofs.FullDofCount();
for (const auto* values : std::array<const Vector*, 6>{
&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.external_force,
&load_scale}) {
if (values->Size() != count)
return Result<DynamicAssembly>::Failure(
Failure("Dynamic trial vector size is invalid."));
for (std::size_t i = 0U; i < count; ++i)
if (!std::isfinite((*values)[i]))
return Result<DynamicAssembly>::Failure(
Failure("Dynamic trial contains nonfinite values."));
}
frame.nodal_internal_order.clear();
frame.nodal_source_ids.clear();
frame.nodal_triad.clear();
frame.nodal_director.clear();
frame.angular_velocity = Vector(3U * domain.Nodes().size());
frame.angular_acceleration = Vector(3U * domain.Nodes().size());
std::vector<DynamicRotationKinematics> rotations;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const auto phi = Triple(frame.generalized_coordinates, 6U * node + 3U);
const auto& baseline = domain.ShellNodeInitialFrames()[node];
auto rotation = EvaluateDynamicRotation(
phi, Vector3(baseline.director), Triple(frame.velocity, 6U * node + 3U),
Triple(frame.acceleration, 6U * node + 3U));
if (!rotation.HasValue())
return Result<DynamicAssembly>::Failure(rotation.GetStatus());
const Matrix r = ExpSkew(phi);
frame.nodal_internal_order.push_back(static_cast<EntityIndex>(node));
frame.nodal_source_ids.push_back(domain.Nodes()[node].source_id);
frame.nodal_director.push_back(rotation.Value().director.Components());
frame.nodal_triad.push_back(
{Transform(r, Vector3(baseline.tangent_a)).Components(),
Transform(r, Vector3(baseline.tangent_b)).Components(),
rotation.Value().director.Components()});
for (std::size_t c = 0U; c < 3U; ++c) {
frame.angular_velocity[3U * node + c] =
rotation.Value().angular_velocity[c];
frame.angular_acceleration[3U * node + c] =
rotation.Value().angular_acceleration[c];
}
rotations.push_back(std::move(rotation.Value()));
}
std::vector<std::optional<NonlinearMitc4InternalEquilibrium>> internal(
shells.size());
std::vector<std::optional<NonlinearMitc4InertiaEvaluation>> inertia(
shells.size());
std::vector<std::optional<Status>> failures(shells.size());
parallel_for.Execute(shells.size(), [&](const std::size_t e) {
const auto q = Gather(frame.generalized_coordinates, shells[e]);
const auto v = Gather(frame.velocity, shells[e]);
const auto a = Gather(frame.acceleration, shells[e]);
NonlinearMitc4TrialDofs trial{};
for (std::size_t node = 0U; node < 4U; ++node) {
trial.displacement[node] = Triple(q, 6U * node);
trial.rotation_vector[node] = Triple(q, 6U * node + 3U);
}
auto f = EvaluateInternalResidualAndTangent(
shells[e].internal, trial, directional_third ? &v : nullptr);
if (!f.HasValue()) {
failures[e] = f.GetStatus();
return;
}
auto m = shells[e].inertia.Evaluate(trial, v, a);
if (!m.HasValue()) {
failures[e] = m.GetStatus();
return;
}
internal[e] = std::move(f.Value());
inertia[e] = std::move(m.Value());
});
frame.inertia_force = Vector(count);
frame.internal_force = Vector(count);
frame.residual = Vector(count);
frame.reaction = Vector(count);
frame.residual_scale = load_scale;
frame.shell_results.clear();
frame.kinetic_energy = frame.physical_strain_energy = frame.drilling_energy =
0.0;
frame.linear_momentum = {};
frame.angular_momentum = {};
frame.linear_momentum_rate = {};
frame.angular_momentum_rate = {};
std::vector<std::map<std::pair<std::size_t, std::size_t>, double>>
constraint_hessians(domain.Nodes().size());
Vector third(count);
Vector third_scale(count);
std::array<std::vector<CooContribution>, 4> entries;
for (auto& values : entries) values.reserve(shells.size() * 24U * 24U);
double momentum_scale = 0.0;
double angular_scale = 0.0;
const double a_coefficient =
1.0 / ((1.0 + parameters.alpha) * parameters.beta * time_increment *
time_increment);
const double v_coefficient =
parameters.gamma / (parameters.beta * time_increment);
for (std::size_t e = 0U; e < shells.size(); ++e) {
if (failures[e].has_value())
return Result<DynamicAssembly>::Failure(*failures[e]);
const auto& f = *internal[e];
const auto& m = *inertia[e];
if (directional_third) {
for (std::size_t local = 0U; local < 4U; ++local) {
auto& hessian = constraint_hessians[shells[e].scatter[6U * local] / 6U];
for (std::size_t i = 0U; i < 24U; ++i)
for (std::size_t j = 0U; j < 24U; ++j) {
hessian[{shells[e].scatter[i], shells[e].scatter[j]}] +=
f.drilling_constraint_hessian[local](i, j);
}
}
}
const auto a = Gather(frame.acceleration, shells[e]);
for (std::size_t row = 0U; row < 24U; ++row) {
const auto global = shells[e].scatter[row];
frame.internal_force[global] += f.internal_force[row];
frame.inertia_force[global] += m.force[row];
third[global] += f.force_directional_second[row];
third_scale[global] += std::abs(f.force_directional_second[row]);
frame.residual_scale[global] += std::abs(m.convective_force[row]) +
std::abs(f.physical_force[row]) +
std::abs(f.drilling_force[row]);
for (std::size_t column = 0U; column < 24U; ++column) {
frame.residual_scale[global] +=
std::abs(m.mass(row, column) * a[column]);
const std::array<double, 4> values{
m.mass(row, column), f.internal_tangent(row, column),
f.internal_tangent(row, column) + m.position_tangent(row, column) +
v_coefficient * m.velocity_tangent(row, column) +
a_coefficient * m.mass(row, column),
f.drilling_metric(row, column)};
for (std::size_t k = 0U; k < 4U; ++k)
entries[k].push_back({global, shells[e].scatter[column], values[k], e,
row * 24U + column});
}
}
for (const auto& point : f.recovery.gauss_points) {
frame.shell_results.push_back(
{shells[e].element_index,
point.location,
point.natural_coordinates,
{point.local_frame.e1, point.local_frame.e2, point.local_frame.e3},
point.generalized_strain,
point.section_resultant,
point.stress});
}
frame.kinetic_energy += m.kinetic_energy;
frame.physical_strain_energy += f.physical_strain_energy;
frame.drilling_energy += f.drilling_energy;
momentum_scale += m.momentum_rate_scale;
angular_scale += m.angular_momentum_rate_scale;
for (std::size_t c = 0U; c < 3U; ++c) {
frame.linear_momentum[c] += m.momentum[c];
frame.angular_momentum[c] += m.angular_momentum[c];
frame.linear_momentum_rate[c] += m.momentum_rate[c];
frame.angular_momentum_rate[c] += m.angular_momentum_rate[c];
}
}
for (std::size_t i = 0U; i < count; ++i) {
frame.residual[i] = frame.inertia_force[i] + frame.internal_force[i] -
frame.external_force[i];
if (!std::isfinite(frame.residual[i]) ||
!std::isfinite(frame.residual_scale[i]) || !std::isfinite(third[i]) ||
!std::isfinite(third_scale[i])) {
return Result<DynamicAssembly>::Failure(
Failure("Dynamic force reduction overflowed."));
}
}
for (const auto i : dofs.ConstrainedDofs())
frame.reaction[i] = frame.residual[i];
std::array<double, 2> residual_norm{};
std::array<double, 2> scale_norm{};
for (const auto i : dofs.FreeDofs()) {
const auto channel = i % 6U < 3U ? 0U : 1U;
residual_norm[channel] =
(std::max)(residual_norm[channel], std::abs(frame.residual[i]));
scale_norm[channel] =
(std::max)(scale_norm[channel], frame.residual_scale[i]);
}
for (std::size_t c = 0U; c < 2U; ++c)
frame.free_residual_metrics[c] = Ratio(residual_norm[c], scale_norm[c]);
auto mass = SparseMatrix::FromCoo(count, count, std::move(entries[0U]),
dofs.GetSparsePattern());
auto stiffness = SparseMatrix::FromCoo(count, count, std::move(entries[1U]),
dofs.GetSparsePattern());
auto effective = SparseMatrix::FromCoo(count, count, std::move(entries[2U]),
dofs.GetSparsePattern());
auto metric = SparseMatrix::FromCoo(count, count, std::move(entries[3U]),
dofs.GetSparsePattern());
for (const auto* result : {&mass, &stiffness, &effective, &metric})
if (!result->HasValue())
return Result<DynamicAssembly>::Failure(result->GetStatus());
Vector hidden_second(domain.Nodes().size());
Vector hidden_second_scale(domain.Nodes().size());
for (std::size_t node = 0U; node < constraint_hessians.size(); ++node) {
for (const auto& entry : constraint_hessians[node]) {
const double term = entry.second * frame.velocity[entry.first.first] *
frame.velocity[entry.first.second];
hidden_second[node] += term;
hidden_second_scale[node] += std::abs(term);
}
}
return Result<DynamicAssembly>::Success(
{std::move(frame), std::move(mass.Value()), std::move(stiffness.Value()),
std::move(effective.Value()), std::move(metric.Value()),
std::move(third), std::move(third_scale), std::move(rotations),
momentum_scale, angular_scale, std::move(hidden_second),
std::move(hidden_second_scale)});
}
Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly,
DynamicAcceptedFrameState* frame) {
static_cast<void>(dofs);
Vector3 total_force;
Vector3 total_moment;
double force_scale = assembly.momentum_rate_scale;
double moment_scale = assembly.angular_momentum_rate_scale;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const auto first = 6U * node;
const Vector3 arm = Vector3(domain.Nodes()[node].coordinates) +
Triple(frame->generalized_coordinates, first);
const Matrix transform = InverseTransposeLeftJacobianSO3(
Triple(frame->generalized_coordinates, first + 3U));
for (const Vector* values : {&frame->external_force, &frame->reaction}) {
const Vector3 force = Triple(*values, first);
const Vector3 moment = Transform(transform, Triple(*values, first + 3U));
total_force = total_force + force;
total_moment = total_moment + arm.Cross(force) + moment;
force_scale += force.Norm();
moment_scale += arm.Cross(force).Norm() + moment.Norm();
}
}
const Vector3 force_error =
total_force - Vector3(frame->linear_momentum_rate);
const Vector3 moment_error =
total_moment - Vector3(frame->angular_momentum_rate);
for (std::size_t c = 0U; c < 3U; ++c) {
frame->global_equilibrium[c] = force_error[c];
frame->global_equilibrium[3U + c] = moment_error[c];
}
if (!force_error.IsFinite() || !moment_error.IsFinite() ||
!std::isfinite(force_scale) || !std::isfinite(moment_scale) ||
Ratio(force_error.Norm(), force_scale) > 1e-8 ||
Ratio(moment_error.Norm(), moment_scale) > 1e-8)
return Failure("Dynamic physical force or moment balance exceeds 1e-8.");
return Status::Ok();
}
} // namespace fesa::hht_internal
+102
View File
@@ -0,0 +1,102 @@
#ifndef FESA_ANALYSIS_HHT_DYNAMIC_INTERNAL_H_
#define FESA_ANALYSIS_HHT_DYNAMIC_INTERNAL_H_
#include <array>
#include <vector>
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/analysis/hht_time_integration.h"
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include "fesa/math/sparse_matrix.h"
namespace fesa {
class LinearSolver;
class ParallelFor;
class TangentSolver;
namespace hht_internal {
/// @brief Keeps immutable local evaluators with the DOF-owner scatter.
struct RuntimeShell {
EntityIndex element_index;
std::array<std::size_t, 24> scatter;
NonlinearMitc4Shell internal;
NonlinearMitc4Inertia inertia;
};
/// @brief Stores a complete trial with independently assembled operators.
struct DynamicAssembly {
DynamicAcceptedFrameState frame;
SparseMatrix mass;
SparseMatrix internal_tangent;
SparseMatrix effective_tangent;
SparseMatrix drilling_metric;
Vector force_directional_second;
Vector force_directional_second_scale;
std::vector<DynamicRotationKinematics> rotations;
double momentum_rate_scale;
double angular_momentum_rate_scale;
Vector hidden_second{0U};
Vector hidden_second_scale{0U};
};
/// @brief Builds checked shell evaluators in stable source order.
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
const DofManager& dofs);
/// @brief Returns the maximum initial active-element edge length.
Result<double> ReferenceLength(const Domain& domain);
/// @brief Assembles dead loads and their uncancelled absolute row scales.
Result<std::array<Vector, 2>> AssembleLoads(const Domain& domain,
const DofManager& dofs);
/// @brief Evaluates current kinetic and internal quantities without commit.
Result<DynamicAssembly> EvaluateAssembly(
const Domain& domain, const DofManager& dofs,
const std::vector<RuntimeShell>& shells, DynamicAcceptedFrameState frame,
const Vector& load_scale, double time_increment,
const HhtParameters& parameters, const ParallelFor& parallel_for,
bool directional_third);
/// @brief Builds deterministic CSR structure from an ordered local list.
Result<SparseMatrix> MakeSparse(std::size_t rows, std::size_t columns,
std::vector<CooContribution> contributions);
/// @brief Builds the scaled free-space operator D^T K D.
Result<SparseMatrix> ScaledFree(const SparseMatrix& matrix,
const DofManager& dofs, double length);
/// @brief Checks normwise backward error in the supplied equation space.
Status CheckLinearSolve(const SparseMatrix& matrix, const Vector& rhs,
const Vector& solution);
/// @brief Finds analytic admissible node null directions without rank cuts.
std::vector<std::size_t> NullNodes(
const DofManager& dofs,
const std::vector<DynamicRotationKinematics>& rotations);
/// @brief Solves the positive-mass and differentiated-null initial equations.
Result<Vector> InitializeAcceleration(const Domain& domain,
const DofManager& dofs,
const DynamicAssembly& initial,
double length,
LinearSolver& positive_solver,
TangentSolver& tangent_solver);
/// @brief Checks mass/regularity and recovers all exact hidden defects.
Status CompleteConstraintEvidence(
const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly, double length,
const std::vector<std::size_t>& initial_null_nodes,
LinearSolver& positive_solver, TangentSolver& tangent_solver,
DynamicAcceptedFrameState* frame);
/// @brief Recovers physical support balance using volume momentum rates.
Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly,
DynamicAcceptedFrameState* frame);
} // namespace hht_internal
} // namespace fesa
#endif // FESA_ANALYSIS_HHT_DYNAMIC_INTERNAL_H_
+322
View File
@@ -0,0 +1,322 @@
#include "fesa/analysis/hht_dynamic_procedure.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <string>
#include <utility>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/solvers/linear/tangent_solver.h"
namespace fesa {
namespace {
using namespace hht_internal;
/// @brief Creates one procedure-owned diagnostic with the source step.
Status Failure(const Domain& domain, const std::string& code,
const std::string& message) {
return Status::Failure(
FailureCategory::kSolver,
{{Severity::kError, code, domain.Dynamic()->dynamic_location, "DYNAMIC",
domain.Dynamic()->source_step_name_raw, message}});
}
/// @brief Checks one update against its uncancelled, dimensionally equal terms.
bool UpdateEqual(const double left, const std::initializer_list<double> terms) {
double right = 0.0;
double scale = 0.0;
for (const double term : terms) {
right += term;
scale += std::abs(term);
}
const double defect = std::abs(left - right);
return std::isfinite(defect) && std::isfinite(scale) &&
(scale == 0.0 ? defect == 0.0 : defect / scale <= 1e-12);
}
/// @brief Verifies both absolute and increment forms before accepting history.
Status CheckUpdates(const Domain& domain, const DynamicAcceptedFrameState& old,
const DynamicAcceptedFrameState& frame,
const HhtPredictor& predictor, const HhtParameters& p,
const double h) {
for (std::size_t i = 0U; i < frame.generalized_coordinates.Size(); ++i) {
const double increment =
frame.generalized_coordinates[i] - old.generalized_coordinates[i];
const double old_term =
h * h * (0.5 - p.beta) * old.algorithmic_acceleration[i];
const double new_term = h * h * p.beta * frame.algorithmic_acceleration[i];
if (!UpdateEqual(frame.generalized_coordinates[i],
{old.generalized_coordinates[i], h * old.velocity[i],
old_term, new_term}) ||
!UpdateEqual(increment, {h * old.velocity[i], old_term, new_term}) ||
!UpdateEqual(
frame.generalized_coordinates[i] - predictor.coordinates[i],
{new_term}) ||
!UpdateEqual(frame.velocity[i],
{old.velocity[i],
h * (1 - p.gamma) * old.algorithmic_acceleration[i],
h * p.gamma * frame.algorithmic_acceleration[i]}) ||
!UpdateEqual(frame.algorithmic_acceleration[i],
{(1 + p.alpha) * frame.acceleration[i],
-p.alpha * old.acceleration[i]})) {
return Failure(
domain, "dynamic-update-inconsistency",
"HHT endpoint update or resolved increment exceeds 1e-12.");
}
}
return Status::Ok();
}
/// @brief Fills the raw channel evidence without hiding a zero denominator.
void ResidualRecord(const DofManager& dofs,
const DynamicAcceptedFrameState& frame,
DynamicNewtonIterationLogRecord* record) {
record->residual_numerator = {};
record->residual_scale = {};
for (const auto full : dofs.FreeDofs()) {
const auto c = full % 6U < 3U ? 0U : 1U;
record->residual_numerator[c] = (std::max)(record->residual_numerator[c],
std::abs(frame.residual[full]));
record->residual_scale[c] =
(std::max)(record->residual_scale[c], frame.residual_scale[full]);
}
record->residual_pass = frame.free_residual_metrics[0U] <= 1e-10 &&
frame.free_residual_metrics[1U] <= 1e-10;
}
/// @brief Forms the four coordinate/rate correction checks in scaled units.
void CorrectionRecord(const Vector& correction, const DofManager& dofs,
const double length, const HhtParameters& p,
DynamicNewtonIterationLogRecord* record) {
record->coordinate_correction = {};
for (const auto full : dofs.FreeDofs()) {
const auto c = full % 6U < 3U ? 0U : 1U;
record->coordinate_correction[c] =
(std::max)(record->coordinate_correction[c],
std::abs(correction[full]) / (c == 0U ? length : 1.0));
}
record->correction_pass = true;
for (std::size_t c = 0; c < 2U; ++c) {
const double dq = record->coordinate_correction[c];
record->velocity_correction[c] = p.gamma / p.beta * dq;
record->acceleration_correction[c] = dq / ((1 + p.alpha) * p.beta);
record->algorithmic_correction[c] = dq / p.beta;
for (const auto* channel :
{&record->coordinate_correction, &record->velocity_correction,
&record->acceleration_correction, &record->algorithmic_correction})
record->correction_pass =
record->correction_pass && (*channel)[c] <= 1e-10;
}
}
} // namespace
HhtDynamicProcedureExecutor::HhtDynamicProcedureExecutor(
const ParallelFor& parallel_for, LinearSolver& positive_solver,
TangentSolver& tangent_solver)
: parallel_for_(parallel_for),
positive_solver_(positive_solver),
tangent_solver_(tangent_solver) {}
Result<DynamicExecutionResult> HhtDynamicProcedureExecutor::Execute(
const Domain& domain, NewtonIterationLogWriter& log) const {
if (domain.Dynamic() == nullptr) {
return Result<DynamicExecutionResult>::Failure(Status::Failure(
FailureCategory::kModel, {{Severity::kError,
"invalid-dynamic-step",
{},
"DYNAMIC",
{},
"HHT requires exactly one dynamic step."}}));
}
auto model = AnalysisModel::Create(domain);
if (!model.HasValue())
return Result<DynamicExecutionResult>::Failure(model.GetStatus());
auto dofs_result = DofManager::Create(model.Value());
if (!dofs_result.HasValue())
return Result<DynamicExecutionResult>::Failure(dofs_result.GetStatus());
const auto& dofs = dofs_result.Value();
auto state_result = DynamicAnalysisState::Create(domain, dofs);
if (!state_result.HasValue())
return Result<DynamicExecutionResult>::Failure(state_result.GetStatus());
auto state = std::move(state_result.Value());
auto shells = BuildRuntimeShells(domain, dofs);
if (!shells.HasValue())
return Result<DynamicExecutionResult>::Failure(shells.GetStatus());
auto length_result = ReferenceLength(domain);
if (!length_result.HasValue())
return Result<DynamicExecutionResult>::Failure(length_result.GetStatus());
const double length = length_result.Value();
auto loads = AssembleLoads(domain, dofs);
if (!loads.HasValue())
return Result<DynamicExecutionResult>::Failure(loads.GetStatus());
auto parameters = MakeHhtParameters(domain.Dynamic()->alpha);
if (!parameters.HasValue())
return Result<DynamicExecutionResult>::Failure(parameters.GetStatus());
const auto p = parameters.Value();
const double h = domain.Dynamic()->initial_increment;
DynamicAcceptedFrameState frame{};
frame.is_initial = true;
for (auto* vector : {&frame.generalized_coordinates, &frame.velocity,
&frame.acceleration, &frame.algorithmic_acceleration})
*vector = Vector(dofs.FullDofCount());
frame.external_force = loads.Value()[0U];
auto initial = EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!initial.HasValue())
return Result<DynamicExecutionResult>::Failure(initial.GetStatus());
const auto null_nodes = NullNodes(dofs, initial.Value().rotations);
auto acceleration = InitializeAcceleration(
domain, dofs, initial.Value(), length, positive_solver_, tangent_solver_);
if (!acceleration.HasValue())
return Result<DynamicExecutionResult>::Failure(acceleration.GetStatus());
frame.acceleration = acceleration.Value();
frame.algorithmic_acceleration = acceleration.Value();
initial = EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!initial.HasValue())
return Result<DynamicExecutionResult>::Failure(initial.GetStatus());
frame = initial.Value().frame;
Status status = CompleteConstraintEvidence(
domain, dofs, initial.Value(), length, null_nodes, positive_solver_,
tangent_solver_, &frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
status = CompleteEquilibrium(domain, dofs, initial.Value(), &frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
const double initial_energy = frame.kinetic_energy +
frame.physical_strain_energy +
frame.drilling_energy;
status = state.CommitAcceptedFrame(frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
DynamicNewtonIterationLogRecord record{};
record.stage = "accepted";
record.outcome = "initial";
record.correction_pass = true;
ResidualRecord(dofs, frame, &record);
status = log.Write(record);
if (status.IsOk()) status = log.Flush();
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
for (std::uint32_t increment = 1U;
increment <= domain.Dynamic()->increment_count; ++increment) {
const auto& old = state.CommittedState();
auto predictor = PredictHht(old.generalized_coordinates, old.velocity,
old.algorithmic_acceleration, h, p);
if (!predictor.HasValue())
return Result<DynamicExecutionResult>::Failure(predictor.GetStatus());
frame = old;
frame.is_initial = false;
frame.frame_index = increment;
frame.increment_index = increment;
frame.time = static_cast<double>(increment) * h;
// A large DIRECT step may put a kinematic predictor outside the shell
// chart before Newton starts. The last accepted geometry is a valid
// initial iterate; q_p still defines the unchanged HHT rate equations.
frame.generalized_coordinates = old.generalized_coordinates;
record = {};
record.increment_index = increment;
record.time = frame.time;
record.correction_pass = true;
bool accepted = false;
for (std::uint32_t iteration = 0U; iteration <= 100U; ++iteration) {
record.iteration_index = iteration;
record.stage = "evaluation";
record.outcome = "candidate";
auto fail = [&](const Status& failure) -> Result<DynamicExecutionResult> {
record.stage = "failed";
record.outcome = "failed";
record.failure_code = failure.Diagnostics().empty()
? "dynamic-solver-failure"
: failure.Diagnostics().front().code;
const Status written = log.Write(record);
return Result<DynamicExecutionResult>::Failure(
written.IsOk() ? failure : written);
};
auto rates = EvaluateHhtRates(frame.generalized_coordinates,
predictor.Value(), old.acceleration, h, p);
if (!rates.HasValue()) return fail(rates.GetStatus());
frame.velocity = std::move(rates.Value().velocity);
frame.acceleration = std::move(rates.Value().acceleration);
frame.algorithmic_acceleration =
std::move(rates.Value().algorithmic_acceleration);
frame.iteration_count = iteration;
auto assembly =
EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, false);
if (!assembly.HasValue()) return fail(assembly.GetStatus());
frame = assembly.Value().frame;
ResidualRecord(dofs, frame, &record);
if (record.residual_pass && record.correction_pass) {
assembly =
EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!assembly.HasValue()) return fail(assembly.GetStatus());
frame = assembly.Value().frame;
status = CompleteConstraintEvidence(
domain, dofs, assembly.Value(), length, null_nodes,
positive_solver_, tangent_solver_, &frame);
if (!status.IsOk()) return fail(status);
status = CompleteEquilibrium(domain, dofs, assembly.Value(), &frame);
if (!status.IsOk()) return fail(status);
status = CheckUpdates(domain, old, frame, predictor.Value(), p, h);
if (!status.IsOk()) return fail(status);
frame.external_work = 0.0;
for (std::size_t i = 0; i < dofs.FullDofCount(); ++i)
frame.external_work +=
frame.external_force[i] * frame.generalized_coordinates[i];
frame.energy_defect =
frame.kinetic_energy + frame.physical_strain_energy +
frame.drilling_energy - initial_energy - frame.external_work;
status = state.CommitAcceptedFrame(frame);
if (!status.IsOk()) return fail(status);
record.stage = "accepted";
record.outcome = "accepted";
status = log.Write(record);
if (status.IsOk()) status = log.Flush();
if (!status.IsOk())
return Result<DynamicExecutionResult>::Failure(status);
accepted = true;
break;
}
if (iteration == 100U)
return fail(Failure(domain, "dynamic-nonconvergence",
"HHT exhausted 100 corrections without satisfying "
"every endpoint check."));
status = log.Write(record);
if (!status.IsOk())
return Result<DynamicExecutionResult>::Failure(status);
auto tangent =
ScaledFree(assembly.Value().effective_tangent, dofs, length);
if (!tangent.HasValue()) return fail(tangent.GetStatus());
Vector rhs(dofs.FreeDofCount());
Vector solution(dofs.FreeDofCount());
Vector correction(dofs.FullDofCount());
for (std::size_t i = 0; i < dofs.FreeDofCount(); ++i) {
const auto full = dofs.FreeDofs()[i];
rhs[i] = -frame.residual[full] * (full % 6U < 3U ? length : 1.0);
}
if (dofs.FreeDofCount() > 0U) {
status = tangent_solver_.FactorizeFresh(tangent.Value());
if (!status.IsOk()) return fail(status);
status = tangent_solver_.SolveCorrection(rhs, solution);
if (!status.IsOk()) return fail(status);
status = CheckLinearSolve(tangent.Value(), rhs, solution);
if (!status.IsOk()) return fail(status);
}
for (std::size_t i = 0; i < dofs.FreeDofCount(); ++i) {
const auto full = dofs.FreeDofs()[i];
correction[full] = solution[i] * (full % 6U < 3U ? length : 1.0);
frame.generalized_coordinates[full] += correction[full];
}
CorrectionRecord(correction, dofs, length, p, &record);
}
if (!accepted)
return Result<DynamicExecutionResult>::Failure(
Failure(domain, "dynamic-nonconvergence",
"Dynamic increment was not accepted."));
}
return Result<DynamicExecutionResult>::Success(
{std::move(state), domain.Warnings()});
}
} // namespace fesa
@@ -0,0 +1,97 @@
#include "fesa/analysis/hht_time_integration.h"
#include <cmath>
#include <utility>
namespace fesa {
namespace {
/// @brief Reports invalid or unrepresentable HHT kinematics.
Status Failure() {
return Status::Failure(
FailureCategory::kSolver,
{{Severity::kError, "dynamic-invalid-state", {}, "DYNAMIC", {},
"HHT requires finite compatible rates, positive time, and coupled "
"coefficients in the supported range."}});
}
/// @brief Validates the coupled coefficient and time denominators.
bool Valid(const double h, const HhtParameters& p) {
return std::isfinite(h) && h > 0.0 && std::isfinite(h * h) &&
h * h > 0.0 && std::isfinite(p.alpha) &&
p.alpha >= -1.0 / 3.0 && p.alpha <= 0.0 &&
p.beta == 0.25 * (1.0 - p.alpha) * (1.0 - p.alpha) &&
p.gamma == 0.5 - p.alpha && p.beta * h * h > 0.0;
}
} // namespace
Result<HhtParameters> MakeHhtParameters(const double alpha) {
if (!std::isfinite(alpha) || alpha < -1.0 / 3.0 || alpha > 0.0) {
return Result<HhtParameters>::Failure(Failure());
}
return Result<HhtParameters>::Success(
{alpha, 0.25 * (1.0 - alpha) * (1.0 - alpha), 0.5 - alpha});
}
Result<HhtPredictor> PredictHht(const Vector& coordinates,
const Vector& velocity,
const Vector& algorithmic_acceleration,
const double time_increment,
const HhtParameters& parameters) {
if (!Valid(time_increment, parameters) ||
coordinates.Size() != velocity.Size() ||
coordinates.Size() != algorithmic_acceleration.Size()) {
return Result<HhtPredictor>::Failure(Failure());
}
HhtPredictor predictor{Vector(coordinates.Size()), Vector(coordinates.Size())};
for (std::size_t i = 0U; i < coordinates.Size(); ++i) {
predictor.coordinates[i] =
coordinates[i] + time_increment * velocity[i] +
time_increment * time_increment * (0.5 - parameters.beta) *
algorithmic_acceleration[i];
predictor.velocity[i] =
velocity[i] + time_increment * (1.0 - parameters.gamma) *
algorithmic_acceleration[i];
if (!std::isfinite(predictor.coordinates[i]) ||
!std::isfinite(predictor.velocity[i])) {
return Result<HhtPredictor>::Failure(Failure());
}
}
return Result<HhtPredictor>::Success(std::move(predictor));
}
Result<HhtRates> EvaluateHhtRates(const Vector& coordinates,
const HhtPredictor& predictor,
const Vector& old_acceleration,
const double time_increment,
const HhtParameters& parameters) {
if (!Valid(time_increment, parameters) ||
coordinates.Size() != predictor.coordinates.Size() ||
coordinates.Size() != predictor.velocity.Size() ||
coordinates.Size() != old_acceleration.Size()) {
return Result<HhtRates>::Failure(Failure());
}
HhtRates rates{Vector(coordinates.Size()), Vector(coordinates.Size()),
Vector(coordinates.Size())};
for (std::size_t i = 0U; i < coordinates.Size(); ++i) {
const double increment = coordinates[i] - predictor.coordinates[i];
rates.algorithmic_acceleration[i] =
increment / (parameters.beta * time_increment * time_increment);
rates.velocity[i] = predictor.velocity[i] +
parameters.gamma * increment /
(parameters.beta * time_increment);
rates.acceleration[i] =
(rates.algorithmic_acceleration[i] +
parameters.alpha * old_acceleration[i]) /
(1.0 + parameters.alpha);
if (!std::isfinite(rates.algorithmic_acceleration[i]) ||
!std::isfinite(rates.acceleration[i]) ||
!std::isfinite(rates.velocity[i])) {
return Result<HhtRates>::Failure(Failure());
}
}
return Result<HhtRates>::Success(std::move(rates));
}
} // namespace fesa
@@ -258,6 +258,68 @@ Result<std::string> SerializeRecord(const NewtonIterationLogRecord& record,
return Result<std::string>::Success(std::move(line));
}
/// @brief Serializes dynamic evidence without static load-factor semantics.
Result<std::string> SerializeRecord(
const DynamicNewtonIterationLogRecord& record,
const std::filesystem::path& log_path) {
const auto time = FormatFiniteNumber(record.time, log_path);
if (!time.HasValue()) return Result<std::string>::Failure(time.GetStatus());
if (record.time < 0.0 || record.stage.empty() || record.outcome.empty()) {
return Result<std::string>::Failure(LogOutputFailure(
log_path,
"A dynamic Newton log requires nonnegative time, stage, and outcome."));
}
std::vector<std::string> fields;
fields.reserve(24U);
AppendField(&fields, "record", "dynamic-newton-iteration");
AppendField(&fields, "schema", "hht-log-v1");
AppendField(&fields, "increment", std::to_string(record.increment_index));
AppendField(&fields, "time", time.Value());
AppendField(&fields, "iteration", std::to_string(record.iteration_index));
AppendField(&fields, "stage", EscapeText(record.stage));
const std::array<const char*, 2U> channels{"u", "phi"};
for (std::size_t channel = 0U; channel < channels.size(); ++channel) {
const std::string suffix{channels[channel]};
const std::array<std::pair<std::string, double>, 6U> metrics{{
{"residual_" + suffix + "_numerator",
record.residual_numerator[channel]},
{"residual_" + suffix + "_scale", record.residual_scale[channel]},
{"coordinate_correction_" + suffix,
record.coordinate_correction[channel]},
{"velocity_correction_" + suffix, record.velocity_correction[channel]},
{"acceleration_correction_" + suffix,
record.acceleration_correction[channel]},
{"algorithmic_correction_" + suffix,
record.algorithmic_correction[channel]},
}};
for (const auto& metric : metrics) {
const auto formatted = FormatFiniteNumber(metric.second, log_path);
if (!formatted.HasValue()) {
return Result<std::string>::Failure(formatted.GetStatus());
}
if (metric.second < 0.0) {
return Result<std::string>::Failure(LogOutputFailure(
log_path, "A dynamic Newton log norm or scale is negative."));
}
AppendField(&fields, metric.first, formatted.Value());
}
AppendField(
&fields, "residual_" + suffix + "_branch",
record.residual_scale[channel] == 0.0 ? "zero-scale" : "relative");
}
AppendField(&fields, "residual_pass", record.residual_pass ? "1" : "0");
AppendField(&fields, "correction_pass", record.correction_pass ? "1" : "0");
AppendField(&fields, "outcome", EscapeText(record.outcome));
AppendField(&fields, "failure_code", EscapeText(record.failure_code));
std::string line;
for (std::size_t index = 0U; index < fields.size(); ++index) {
if (index != 0U) line += '\t';
line += fields[index];
}
line += '\n';
return Result<std::string>::Success(std::move(line));
}
} // namespace
Status NewtonIterationLogWriter::Open(const std::filesystem::path& log_path) {
@@ -296,6 +358,27 @@ Status NewtonIterationLogWriter::Write(const NewtonIterationLogRecord& record) {
return Status::Ok();
}
Status NewtonIterationLogWriter::Write(
const DynamicNewtonIterationLogRecord& record) {
if (state_ != State::kOpen) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log writer is not open.");
}
const auto line = SerializeRecord(record, log_path_);
if (!line.HasValue()) {
state_ = State::kFailed;
return line.GetStatus();
}
stream_.write(line.Value().data(),
static_cast<std::streamsize>(line.Value().size()));
if (!stream_) {
state_ = State::kFailed;
return LogOutputFailure(log_path_,
"The dynamic Newton log record could not write.");
}
return Status::Ok();
}
Status NewtonIterationLogWriter::Flush() {
if (state_ != State::kOpen) {
state_ = State::kFailed;
+35 -3
View File
@@ -2,6 +2,7 @@
#include <utility>
#include "fesa/analysis/hht_dynamic_procedure.h"
#include "fesa/analysis/linear_frequency_procedure.h"
#include "fesa/analysis/linear_static_procedure.h"
#include "fesa/analysis/newton_iteration_log.h"
@@ -9,6 +10,7 @@
#include "fesa/app/output_paths.h"
#include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.h"
#include "fesa/results/dynamic_results_writer.h"
#include "fesa/results/eigen_results_writer.h"
#include "fesa/results/nonlinear_results_writer.h"
#include "fesa/results/results_writer.h"
@@ -37,7 +39,8 @@ Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
NonlinearStaticProcedureExecutor& nonlinear_executor,
const LinearFrequencyProcedureExecutor& frequency_executor,
NewtonIterationLogWriter& log) {
NewtonIterationLogWriter& log,
const HhtDynamicProcedureExecutor* dynamic_executor) {
if (domain.Steps().Size() != 1U) {
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
FailureCategory::kInput,
@@ -49,6 +52,22 @@ Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
"Prepared analysis requires exactly one analysis step."}}));
}
if (domain.Dynamic() != nullptr) {
if (dynamic_executor == nullptr)
return Result<PreparedAnalysisOutcome>::Failure(
Status::Failure(FailureCategory::kSolver,
{{Severity::kError,
"missing-dynamic-executor",
{},
"DYNAMIC",
{},
"Dynamic execution adapter is required."}}));
auto dynamic = dynamic_executor->Execute(domain, log);
if (!dynamic.HasValue())
return Result<PreparedAnalysisOutcome>::Failure(dynamic.GetStatus());
return Result<PreparedAnalysisOutcome>::Success(
PreparedAnalysisOutcome{std::move(dynamic.Value())});
}
if (domain.Steps()[0U].IsFrequency()) {
auto eigen = frequency_executor.Execute(domain);
if (!eigen.HasValue()) {
@@ -80,8 +99,21 @@ Status FinalizePreparedAnalysisResult(
const DerivedOutputPaths& paths, const Domain& domain,
const PreparedAnalysisOutcome& outcome,
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
NonlinearResultsWriter& nonlinear_writer,
EigenResultsWriter& eigen_writer) {
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer,
DynamicResultsWriter* dynamic_writer) {
if (std::holds_alternative<DynamicExecutionResult>(outcome)) {
if (dynamic_writer == nullptr)
return Status::Failure(FailureCategory::kOutput,
{{Severity::kError,
"missing-dynamic-writer",
{},
"DYNAMIC",
{},
"Dynamic result writer is required."}});
return dynamic_writer->Write(
paths.results_path, domain,
std::get<DynamicExecutionResult>(outcome).state, diagnostics);
}
if (std::holds_alternative<EigenAnalysisState>(outcome)) {
return eigen_writer.Write(paths.results_path, domain,
std::get<EigenAnalysisState>(outcome),