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
+7
View File
@@ -3,7 +3,12 @@ add_library(
STATIC
analysis/analysis_model.cpp
analysis/analysis_state.cpp
analysis/dynamic_analysis_state.cpp
analysis/eigen_reduction.cpp
analysis/hht_time_integration.cpp
analysis/hht_dynamic_algebra.cpp
analysis/hht_dynamic_assembly.cpp
analysis/hht_dynamic_procedure.cpp
analysis/linear_frequency_procedure.cpp
analysis/linear_static_analysis.cpp
analysis/linear_static_procedure.cpp
@@ -29,6 +34,7 @@ add_library(
elements/finite_rotation.cpp
elements/mitc4_shell.cpp
elements/nonlinear_mitc4_shell.cpp
elements/nonlinear_mitc4_inertia.cpp
fem/dof_manager.cpp
io/abaqus/domain_builder.cpp
io/abaqus/domain_mapping_context.cpp
@@ -39,6 +45,7 @@ add_library(
io/abaqus/surface_definition_mapper.cpp
io/abaqus/topology_mapper.cpp
io/hdf5/hdf5_atomic_file.cpp
io/hdf5/dynamic_hdf5_results_writer.cpp
io/hdf5/hdf5_model_writer.cpp
io/hdf5/nonlinear_hdf5_results_writer.cpp
io/hdf5/hdf5_primitives.cpp
@@ -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),
+9 -4
View File
@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#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"
@@ -13,6 +14,7 @@
#include "fesa/app/output_paths.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/core/diagnostic.h"
#include "fesa/io/hdf5/dynamic_hdf5_results_writer.h"
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
#include "fesa/io/hdf5/hdf5_results_writer.h"
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
@@ -84,6 +86,7 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
Hdf5ResultsWriter linear_writer;
NonlinearHdf5ResultsWriter nonlinear_writer;
EigenHdf5ResultsWriter eigen_writer;
DynamicHdf5ResultsWriter dynamic_writer;
NewtonIterationLogWriter log;
Status status = log.Open(paths.Value().log_path);
@@ -101,9 +104,11 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
NonlinearStaticProcedureExecutor nonlinear_executor(parallel_for,
tangent_solver);
const LinearFrequencyProcedureExecutor frequency_executor;
Result<PreparedAnalysisOutcome> outcome =
ExecutePreparedAnalysis(prepared.Value().domain, linear_executor,
nonlinear_executor, frequency_executor, log);
const HhtDynamicProcedureExecutor dynamic_executor(
parallel_for, linear_solver, tangent_solver);
Result<PreparedAnalysisOutcome> outcome = ExecutePreparedAnalysis(
prepared.Value().domain, linear_executor, nonlinear_executor,
frequency_executor, log, &dynamic_executor);
if (!outcome.HasValue()) {
status = outcome.GetStatus();
} else {
@@ -115,7 +120,7 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
status = FinalizePreparedAnalysisResult(
paths.Value(), prepared.Value().domain, outcome.Value(),
prepared.Value().diagnostics, linear_writer, nonlinear_writer,
eigen_writer);
eigen_writer, &dynamic_writer);
if (status.IsOk()) {
return kSuccessExitCode;
}
@@ -0,0 +1,871 @@
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <string>
#include <utility>
#include "fesa/elements/finite_rotation.h"
namespace fesa {
namespace {
constexpr std::size_t kNodes = 4U;
constexpr std::size_t kDofs = 24U;
const std::array<double, 3> kGauss{-std::sqrt(3.0 / 5.0), 0.0,
std::sqrt(3.0 / 5.0)};
constexpr std::array<double, 3> kWeights{5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0};
template <class T>
Result<T> Failure(const std::string& code, const SourceLocation& location,
const std::string& identity, std::string message) {
return Result<T>::Failure(Status::Failure(
FailureCategory::kModel, {{Severity::kError, code, location, "*ELEMENT",
identity, std::move(message)}}));
}
std::string Identity(const std::array<const Node*, kNodes>& nodes) {
std::string identity;
for (const Node* node : nodes) {
if (!identity.empty()) {
identity += "-";
}
identity += node == nullptr ? "?" : node->source_id.source_label_text;
}
return identity;
}
Matrix FrameMatrix(const ShellNodeInitialFrame& frame) {
Matrix result{3U, 3U};
for (std::size_t row = 0U; row < 3U; ++row) {
result(row, 0U) = frame.tangent_a[row];
result(row, 1U) = frame.tangent_b[row];
result(row, 2U) = frame.director[row];
}
return result;
}
bool IsFinite(const Matrix& matrix) {
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
for (std::size_t column = 0U; column < matrix.Columns(); ++column) {
if (!std::isfinite(matrix(row, column))) {
return false;
}
}
}
return true;
}
bool IsFinite(const Vector& vector) {
for (std::size_t index = 0U; index < vector.Size(); ++index) {
if (!std::isfinite(vector[index])) {
return false;
}
}
return true;
}
/// @brief Carries all derivatives through order three in one nodal rotation.
struct Jet {
double value{};
std::array<double, 3> d{};
std::array<std::array<double, 3>, 3> dd{};
std::array<std::array<std::array<double, 3>, 3>, 3> ddd{};
};
Jet Constant(const double value) { return Jet{value, {}, {}, {}}; }
Jet Variable(const double value, const std::size_t component) {
Jet result = Constant(value);
result.d[component] = 1.0;
return result;
}
Jet operator+(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value + right.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = left.d[i] + right.d[i];
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] = left.dd[i][j] + right.dd[i][j];
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] = left.ddd[i][j][k] + right.ddd[i][j][k];
}
}
}
return result;
}
Jet operator-(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value - right.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = left.d[i] - right.d[i];
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] = left.dd[i][j] - right.dd[i][j];
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] = left.ddd[i][j][k] - right.ddd[i][j][k];
}
}
}
return result;
}
Jet operator*(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value * right.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = left.d[i] * right.value + left.value * right.d[i];
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] = left.dd[i][j] * right.value + left.d[i] * right.d[j] +
left.d[j] * right.d[i] + left.value * right.dd[i][j];
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] =
left.ddd[i][j][k] * right.value + left.dd[i][j] * right.d[k] +
left.dd[i][k] * right.d[j] + left.dd[j][k] * right.d[i] +
left.d[i] * right.dd[j][k] + left.d[j] * right.dd[i][k] +
left.d[k] * right.dd[i][j] + left.value * right.ddd[i][j][k];
}
}
}
return result;
}
Jet Reciprocal(const Jet& value) {
Jet result{};
result.value = 1.0 / value.value;
const double squared = result.value * result.value;
const double cubed = squared * result.value;
const double fourth = cubed * result.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = -value.d[i] * squared;
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] =
2.0 * value.d[i] * value.d[j] * cubed - value.dd[i][j] * squared;
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] =
-6.0 * value.d[i] * value.d[j] * value.d[k] * fourth +
2.0 *
(value.dd[i][j] * value.d[k] + value.dd[i][k] * value.d[j] +
value.dd[j][k] * value.d[i]) *
cubed -
value.ddd[i][j][k] * squared;
}
}
}
return result;
}
Jet InverseSqrt(const Jet& value) {
Jet result{};
result.value = 1.0 / std::sqrt(value.value);
const double cubed = result.value * result.value * result.value;
const double fifth = cubed * result.value * result.value;
const double seventh = fifth * result.value * result.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = -0.5 * cubed * value.d[i];
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] =
0.75 * fifth * value.d[i] * value.d[j] - 0.5 * cubed * value.dd[i][j];
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] =
-1.875 * seventh * value.d[i] * value.d[j] * value.d[k] +
0.75 * fifth *
(value.dd[i][j] * value.d[k] + value.dd[i][k] * value.d[j] +
value.dd[j][k] * value.d[i]) -
0.5 * cubed * value.ddd[i][j][k];
}
}
}
return result;
}
Jet Scale(const double scalar, const Jet& value) {
return Constant(scalar) * value;
}
using JetVector = std::array<Jet, 3>;
using JetMatrix = std::array<std::array<Jet, 3>, 3>;
JetVector Multiply(const JetMatrix& matrix, const JetVector& vector) {
JetVector result{Constant(0.0), Constant(0.0), Constant(0.0)};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row] = result[row] + matrix[row][column] * vector[column];
}
}
return result;
}
JetMatrix Multiply(const JetMatrix& left, const JetMatrix& right) {
JetMatrix result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = Constant(0.0);
for (std::size_t index = 0U; index < 3U; ++index) {
result[row][column] =
result[row][column] + left[row][index] * right[index][column];
}
}
}
return result;
}
JetMatrix IdentityMatrix() {
JetMatrix result{};
for (std::size_t index = 0U; index < 3U; ++index) {
result[index][index] = Constant(1.0);
}
return result;
}
JetMatrix Skew(const JetVector& value) {
JetMatrix result{};
result[0U][1U] = Scale(-1.0, value[2U]);
result[0U][2U] = value[1U];
result[1U][0U] = value[2U];
result[1U][2U] = Scale(-1.0, value[0U]);
result[2U][0U] = Scale(-1.0, value[1U]);
result[2U][1U] = value[0U];
return result;
}
Jet Dot(const JetVector& left, const JetVector& right) {
Jet result = Constant(0.0);
for (std::size_t component = 0U; component < 3U; ++component) {
result = result + left[component] * right[component];
}
return result;
}
JetMatrix Inverse(const JetMatrix& matrix) {
JetMatrix cofactor{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
const std::size_t a = (row + 1U) % 3U;
const std::size_t b = (row + 2U) % 3U;
const std::size_t c = (column + 1U) % 3U;
const std::size_t d = (column + 2U) % 3U;
cofactor[row][column] =
matrix[a][c] * matrix[b][d] - matrix[a][d] * matrix[b][c];
// Cyclic minor ordering already includes the cofactor sign.
}
}
Jet determinant = Constant(0.0);
for (std::size_t column = 0U; column < 3U; ++column) {
determinant = determinant + matrix[0U][column] * cofactor[0U][column];
}
const Jet inverse_determinant = Reciprocal(determinant);
JetMatrix result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = cofactor[column][row] * inverse_determinant;
}
}
return result;
}
JetMatrix LeftJacobian(const JetVector& phi) {
const JetMatrix skew = Skew(phi);
JetMatrix result = IdentityMatrix();
JetMatrix term = IdentityMatrix();
double factorial = 1.0;
for (std::size_t order = 1U; order <= 32U; ++order) {
term = Multiply(term, skew);
factorial *= static_cast<double>(order + 1U);
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] =
result[row][column] + Scale(1.0 / factorial, term[row][column]);
}
}
}
return result;
}
/// @brief Uses a converged exponential series so D1/D2/D3 share one analytic
/// map.
JetMatrix ExpSkew(const JetVector& phi) {
const JetMatrix skew = Skew(phi);
JetMatrix result = IdentityMatrix();
JetMatrix term = IdentityMatrix();
double factorial = 1.0;
for (std::size_t order = 1U; order <= 32U; ++order) {
term = Multiply(term, skew);
factorial *= static_cast<double>(order);
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] =
result[row][column] + Scale(1.0 / factorial, term[row][column]);
}
}
}
return result;
}
double Determinant(const Vector3& a, const Vector3& b, const Vector3& c) {
return a.Dot(b.Cross(c));
}
/// @brief Checks the scaled physical twenty-dimensional mass without a rank
/// cut.
Status CheckPhysicalMass(const Matrix& mass,
const std::array<Vector3, kNodes>& nulls,
const double length, const SourceLocation& location,
const std::string& identity) {
const auto fail = [&](const std::string& code, const std::string& message) {
return Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, "*ELEMENT", identity, message}});
};
constexpr std::size_t kPhysical = 20U;
Matrix scaled{kDofs, kDofs};
double scale = 0.0;
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kDofs; ++column) {
scaled(row, column) = mass(row, column) * (row % 6U < 3U ? length : 1.0) *
(column % 6U < 3U ? length : 1.0);
if (mass(row, column) != 0.0 && scaled(row, column) == 0.0) {
return fail("invalid-nonlinear-inertia-positive-mass",
"Physical mass scaling underflowed a nonzero entry.");
}
scale = std::max(scale, std::abs(scaled(row, column)));
}
}
if (!IsFinite(scaled) || !std::isfinite(scale) || !(scale > 0.0)) {
return fail("invalid-nonlinear-inertia-positive-mass",
"Scaled physical mass must have a finite positive scale.");
}
// Scaling by a positive entry preserves conditioning and avoids density
// overflow in the Cholesky and inverse-action norms.
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kDofs; ++column) {
scaled(row, column) /= scale;
}
}
Matrix complement{kDofs, kPhysical};
for (std::size_t node = 0U; node < kNodes; ++node) {
const Vector3& normal = nulls[node];
std::array<std::size_t, 3> axes{0U, 1U, 2U};
std::stable_sort(axes.begin(), axes.end(),
[&](const std::size_t a, const std::size_t b) {
return std::abs(normal[a]) < std::abs(normal[b]);
});
std::array<Vector3, 2> tangent{};
for (std::size_t column = 0U; column < 2U; ++column) {
std::array<double, 3> axis{};
axis[axes[column]] = 1.0;
Vector3 direction{axis};
direction = direction - direction.Dot(normal) * normal;
for (std::size_t previous = 0U; previous < column; ++previous) {
direction =
direction - direction.Dot(tangent[previous]) * tangent[previous];
}
const double norm = direction.Norm();
if (!std::isfinite(norm) || !(norm > 0.0)) {
return fail("invalid-nonlinear-inertia-null",
"The analytic physical complement must remain finite.");
}
tangent[column] = direction / norm;
}
for (std::size_t row = 0U; row < 3U; ++row) {
complement(6U * node + row, 5U * node + row) = 1.0;
for (std::size_t column = 0U; column < 2U; ++column) {
complement(6U * node + 3U + row, 5U * node + 3U + column) =
tangent[column][row];
}
}
}
double mass_norm = 0.0;
double symmetry_norm = 0.0;
double null_action_norm = 0.0;
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kDofs; ++column) {
mass_norm = std::hypot(mass_norm, scaled(row, column));
symmetry_norm =
std::hypot(symmetry_norm, scaled(row, column) - scaled(column, row));
}
for (std::size_t node = 0U; node < kNodes; ++node) {
double action = 0.0;
for (std::size_t component = 0U; component < 3U; ++component) {
action +=
scaled(row, 6U * node + 3U + component) * nulls[node][component];
}
null_action_norm = std::hypot(null_action_norm, action);
}
}
double gram_error = 0.0;
double orthogonality_error = 0.0;
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
double entry = row == column ? -1.0 : 0.0;
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
entry += complement(inner, row) * complement(inner, column);
}
gram_error = std::hypot(gram_error, entry);
}
for (std::size_t node = 0U; node < kNodes; ++node) {
double entry = 0.0;
for (std::size_t component = 0U; component < 3U; ++component) {
entry += complement(6U * node + 3U + component, row) *
nulls[node][component];
}
orthogonality_error = std::hypot(orthogonality_error, entry);
}
}
double null_gram_error = 0.0;
for (const Vector3& normal : nulls) {
null_gram_error = std::hypot(null_gram_error, normal.Dot(normal) - 1.0);
}
const double full_gram_error = std::hypot(
gram_error,
std::hypot(null_gram_error, std::sqrt(2.0) * orthogonality_error));
if (!(symmetry_norm / mass_norm <= 1.0e-12) ||
!(null_action_norm / mass_norm / 2.0 <= 1.0e-12) ||
!(full_gram_error <= 1.0e-12)) {
return fail("invalid-nonlinear-inertia-null",
"Scaled mass symmetry and analytic null identities failed.");
}
Matrix block{kPhysical, kPhysical};
Matrix mass_complement{kDofs, kPhysical};
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
mass_complement(row, column) +=
scaled(row, inner) * complement(inner, column);
}
}
}
double block_norm = 0.0;
for (std::size_t row = 0U; row < kPhysical; ++row) {
double row_sum = 0.0;
for (std::size_t column = 0U; column < kPhysical; ++column) {
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
block(row, column) +=
complement(inner, row) * mass_complement(inner, column);
}
row_sum += std::abs(block(row, column));
}
block_norm = std::max(block_norm, row_sum);
}
Matrix lower{kPhysical, kPhysical};
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column <= row; ++column) {
double entry = block(row, column);
for (std::size_t inner = 0U; inner < column; ++inner) {
entry -= lower(row, inner) * lower(column, inner);
}
if (!std::isfinite(entry) || (row == column && !(entry > 0.0))) {
return fail(
"invalid-nonlinear-inertia-positive-mass",
"Physical mass requires strictly positive Cholesky pivots.");
}
lower(row, column) =
row == column ? std::sqrt(entry) : entry / lower(column, column);
}
}
std::array<double, kPhysical> inverse_row_sums{};
double reconstruction_error = 0.0;
double block_frobenius = 0.0;
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
double reconstructed = 0.0;
for (std::size_t inner = 0U; inner <= std::min(row, column); ++inner) {
reconstructed += lower(row, inner) * lower(column, inner);
}
reconstruction_error =
std::hypot(reconstruction_error, reconstructed - block(row, column));
block_frobenius = std::hypot(block_frobenius, block(row, column));
}
}
if (!(reconstruction_error / block_frobenius <= 1.0e-12)) {
return fail("invalid-nonlinear-inertia-positive-mass",
"Physical mass Cholesky reconstruction failed.");
}
for (std::size_t column = 0U; column < kPhysical; ++column) {
std::array<double, kPhysical> solution{};
for (std::size_t row = 0U; row < kPhysical; ++row) {
double rhs = row == column ? 1.0 : 0.0;
for (std::size_t inner = 0U; inner < row; ++inner) {
rhs -= lower(row, inner) * solution[inner];
}
solution[row] = rhs / lower(row, row);
}
for (std::size_t reverse = kPhysical; reverse > 0U; --reverse) {
const std::size_t row = reverse - 1U;
double rhs = solution[row];
for (std::size_t inner = row + 1U; inner < kPhysical; ++inner) {
rhs -= lower(inner, row) * solution[inner];
}
solution[row] = rhs / lower(row, row);
inverse_row_sums[row] += std::abs(solution[row]);
}
}
const double inverse_norm =
*std::max_element(inverse_row_sums.begin(), inverse_row_sums.end());
const double reciprocal_condition = 1.0 / block_norm / inverse_norm;
if (!std::isfinite(inverse_norm) || !std::isfinite(reciprocal_condition) ||
!(reciprocal_condition >= std::ldexp(1.0, -32))) {
return fail("ill-conditioned-nonlinear-inertia-positive-mass",
"Physical mass reciprocal infinity condition is below 2^-32.");
}
return Status::Ok();
}
} // namespace
Result<DynamicRotationKinematics> EvaluateDynamicRotation(
const Vector3& phi_value, const Vector3& initial_director,
const Vector3& velocity, const Vector3& acceleration) {
if (!phi_value.IsFinite() || !initial_director.IsFinite() ||
!velocity.IsFinite() || !acceleration.IsFinite() ||
!(std::abs(initial_director.Norm() - 1.0) <= 1.0e-12) ||
!(std::acos(-1.0) - phi_value.Norm() >
std::sqrt(std::numeric_limits<double>::epsilon()))) {
return Failure<DynamicRotationKinematics>(
"invalid-dynamic-rotation", {{}, 0U}, "DYNAMIC_ROTATION",
"Dynamic rotation requires finite principal coordinates and a unit "
"initial director.");
}
JetVector phi{};
for (std::size_t component = 0U; component < 3U; ++component) {
phi[component] = Variable(phi_value[component], component);
}
const JetVector initial{Constant(initial_director.X()),
Constant(initial_director.Y()),
Constant(initial_director.Z())};
const JetVector director_jet = Multiply(ExpSkew(phi), initial);
const JetMatrix left_jacobian = LeftJacobian(phi);
JetVector null_jet = Multiply(Inverse(left_jacobian), director_jet);
const Jet null_norm_squared = Dot(null_jet, null_jet);
if (!std::isfinite(null_norm_squared.value) ||
!(null_norm_squared.value > 0.0)) {
return Failure<DynamicRotationKinematics>(
"invalid-dynamic-rotation", {{}, 0U}, "DYNAMIC_ROTATION",
"Dynamic rotation produced a nonpositive drilling-null norm.");
}
const Jet inverse_norm = InverseSqrt(null_norm_squared);
for (Jet& component : null_jet) {
component = component * inverse_norm;
}
std::array<double, 3> directional_second{};
std::array<double, 3> angular_velocity{};
std::array<double, 3> angular_acceleration{};
Matrix gradient{3U, 3U};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
gradient(row, column) = null_jet[row].d[column];
}
}
for (std::size_t row = 0U; row < 3U; ++row) {
double second = 0.0;
double omega = 0.0;
double omega_dot = 0.0;
for (std::size_t column = 0U; column < 3U; ++column) {
omega += left_jacobian[row][column].value * velocity[column];
omega_dot += left_jacobian[row][column].value * acceleration[column];
for (std::size_t other = 0U; other < 3U; ++other) {
second += null_jet[row].dd[column][other] * velocity[column] *
velocity[other];
omega_dot += left_jacobian[row][column].d[other] * velocity[other] *
velocity[column];
}
}
directional_second[row] = second;
angular_velocity[row] = omega;
angular_acceleration[row] = omega_dot;
}
const Vector3 director{director_jet[0U].value, director_jet[1U].value,
director_jet[2U].value};
const Vector3 drill_null{null_jet[0U].value, null_jet[1U].value,
null_jet[2U].value};
const Vector3 directional_second_value{directional_second};
const Vector3 angular_velocity_value{angular_velocity};
const Vector3 angular_acceleration_value{angular_acceleration};
if (!director.IsFinite() || !drill_null.IsFinite() ||
!directional_second_value.IsFinite() ||
!angular_velocity_value.IsFinite() ||
!angular_acceleration_value.IsFinite() || !IsFinite(gradient)) {
return Failure<DynamicRotationKinematics>(
"invalid-dynamic-rotation", {{}, 0U}, "DYNAMIC_ROTATION",
"Dynamic rotation evaluation must remain finite.");
}
DynamicRotationKinematics result{director,
drill_null,
std::move(gradient),
directional_second_value,
angular_velocity_value,
angular_acceleration_value,
{},
{},
{}};
for (std::size_t i = 0U; i < 3U; ++i) {
result.director_first[i] = {director_jet[0].d[i], director_jet[1].d[i],
director_jet[2].d[i]};
for (std::size_t j = 0U; j < 3U; ++j) {
result.director_second[i][j] = {director_jet[0].dd[i][j],
director_jet[1].dd[i][j],
director_jet[2].dd[i][j]};
for (std::size_t k = 0U; k < 3U; ++k) {
result.director_third[i][j][k] = {director_jet[0].ddd[i][j][k],
director_jet[1].ddd[i][j][k],
director_jet[2].ddd[i][j][k]};
}
}
}
return Result<DynamicRotationKinematics>::Success(std::move(result));
}
NonlinearMitc4Inertia::NonlinearMitc4Inertia(
std::array<Vector3, 4> coordinates, std::array<Matrix, 4> initial_frames,
std::array<Vector3, 4> initial_directors, const double thickness,
const double density, SourceLocation source_location, std::string identity)
: coordinates_(std::move(coordinates)),
initial_frames_(std::move(initial_frames)),
initial_directors_(std::move(initial_directors)),
thickness_(thickness),
density_(density),
source_location_(std::move(source_location)),
identity_(std::move(identity)) {}
Result<NonlinearMitc4Inertia> NonlinearMitc4Inertia::Create(
std::array<const Node*, 4> nodes,
std::array<ShellNodeInitialFrame, 4> initial_frames,
const ShellSection& section, const LinearElasticMaterial& material,
const double density) {
const std::string identity = Identity(nodes);
if (!std::isfinite(density) || !(density > 0.0)) {
return Failure<NonlinearMitc4Inertia>(
"invalid-nonlinear-inertia-density", section.location, identity,
"Nonlinear MITC4 inertia requires a finite positive density.");
}
const auto shell =
NonlinearMitc4Shell::Create(nodes, initial_frames, section, material);
if (!shell.HasValue()) {
return Result<NonlinearMitc4Inertia>::Failure(shell.GetStatus());
}
std::array<Vector3, kNodes> coordinates{};
std::array<Matrix, kNodes> frames{Matrix{3U, 3U}, Matrix{3U, 3U},
Matrix{3U, 3U}, Matrix{3U, 3U}};
std::array<Vector3, kNodes> directors{};
for (std::size_t node = 0U; node < kNodes; ++node) {
coordinates[node] = Vector3{nodes[node]->coordinates};
frames[node] = FrameMatrix(initial_frames[node]);
directors[node] = Vector3{initial_frames[node].director};
}
NonlinearMitc4Inertia inertia{
coordinates, std::move(frames), directors, section.thickness,
density, nodes[0U]->location, identity};
const auto initial = inertia.Evaluate({}, Vector{kDofs}, Vector{kDofs});
if (!initial.HasValue()) {
return Result<NonlinearMitc4Inertia>::Failure(initial.GetStatus());
}
return Result<NonlinearMitc4Inertia>::Success(std::move(inertia));
}
Result<NonlinearMitc4InertiaEvaluation> NonlinearMitc4Inertia::Evaluate(
const NonlinearMitc4TrialDofs& trial, const Vector& velocity,
const Vector& acceleration) const {
if (velocity.Size() != kDofs || acceleration.Size() != kDofs ||
!IsFinite(velocity) || !IsFinite(acceleration)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-rate", source_location_, identity_,
"Nonlinear MITC4 inertia requires 24 finite rates.");
}
std::array<DynamicRotationKinematics, kNodes> rotations{};
std::array<Vector3, kNodes> nulls{};
double length = 0.0;
for (std::size_t node = 0U; node < kNodes; ++node) {
length = std::max(
length,
(coordinates_[(node + 1U) % kNodes] - coordinates_[node]).Norm());
if (!trial.displacement[node].IsFinite() ||
!MakeFiniteRotationState(trial.rotation_vector[node],
initial_frames_[node])
.HasValue()) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-trial", source_location_, identity_,
"Nonlinear MITC4 inertia trial must be in the principal chart.");
}
const auto rotation = EvaluateDynamicRotation(
trial.rotation_vector[node], initial_directors_[node], {}, {});
if (!rotation.HasValue()) {
return Result<NonlinearMitc4InertiaEvaluation>::Failure(
rotation.GetStatus());
}
rotations[node] = rotation.Value();
nulls[node] = rotation.Value().drill_null;
for (std::size_t previous = 0U; previous < node; ++previous) {
if (!(rotations[node].director.Dot(rotations[previous].director) >
-1.0 + 1.0e-8)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-director", source_location_, identity_,
"Nonlinear MITC4 inertia trial directors must not be opposed.");
}
}
}
if (!std::isfinite(length) || !(length > 0.0)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-geometry", source_location_, identity_,
"Nonlinear MITC4 inertia requires a finite positive initial edge.");
}
NonlinearMitc4InertiaEvaluation result{Matrix{kDofs, kDofs},
Matrix{kDofs, kDofs},
Matrix{kDofs, kDofs},
Vector{kDofs},
Vector{kDofs},
0.0,
{},
{},
{},
{}};
for (std::size_t xi = 0U; xi < kGauss.size(); ++xi) {
for (std::size_t eta = 0U; eta < kGauss.size(); ++eta) {
const Mitc4ShapeFunctions shape =
Mitc4Shell::ShapeFunctions(kGauss[xi], kGauss[eta]);
for (std::size_t zeta = 0U; zeta < kGauss.size(); ++zeta) {
const double z = 0.5 * thickness_ * kGauss[zeta];
const double weight = kWeights[xi] * kWeights[eta] * kWeights[zeta];
Vector3 ref_xi{};
Vector3 ref_eta{};
Vector3 ref_zeta{};
Vector3 current_xi{};
Vector3 current_eta{};
Vector3 current_zeta{};
Vector3 position{};
Vector3 velocity_point{};
Vector3 acceleration_point{};
Vector3 convective_point{};
std::array<Vector3, kDofs> h{};
std::array<Vector3, kDofs> velocity_derivative{};
std::array<Vector3, kDofs> position_derivative{};
for (std::size_t node = 0U; node < kNodes; ++node) {
const std::size_t base = 6U * node;
const auto& rotation = rotations[node];
const Vector3 reference =
coordinates_[node] + z * initial_directors_[node];
const Vector3 current = coordinates_[node] +
trial.displacement[node] +
z * rotation.director;
ref_xi = ref_xi + shape.xi_derivatives[node] * reference;
ref_eta = ref_eta + shape.eta_derivatives[node] * reference;
ref_zeta = ref_zeta + 0.5 * thickness_ * shape.values[node] *
initial_directors_[node];
current_xi = current_xi + shape.xi_derivatives[node] * current;
current_eta = current_eta + shape.eta_derivatives[node] * current;
current_zeta = current_zeta + 0.5 * thickness_ * shape.values[node] *
rotation.director;
position = position + shape.values[node] * current;
for (std::size_t component = 0U; component < 3U; ++component) {
std::array<double, 3> translation{};
translation[component] = shape.values[node];
h[base + component] = Vector3{translation};
h[base + 3U + component] =
z * shape.values[node] * rotation.director_first[component];
for (std::size_t other = 0U; other < 3U; ++other) {
const Vector3 second = z * shape.values[node] *
rotation.director_second[component][other];
convective_point =
convective_point + second * velocity[base + 3U + component] *
velocity[base + 3U + other];
velocity_derivative[base + 3U + component] =
velocity_derivative[base + 3U + component] +
2.0 * second * velocity[base + 3U + other];
position_derivative[base + 3U + component] =
position_derivative[base + 3U + component] +
second * acceleration[base + 3U + other];
for (std::size_t third = 0U; third < 3U; ++third) {
position_derivative[base + 3U + component] =
position_derivative[base + 3U + component] +
z * shape.values[node] *
rotation.director_third[component][other][third] *
velocity[base + 3U + other] *
velocity[base + 3U + third];
}
}
}
}
const double jacobian = Determinant(ref_xi, ref_eta, ref_zeta);
const double current_jacobian =
Determinant(current_xi, current_eta, current_zeta);
if (!std::isfinite(jacobian) || !(jacobian > 0.0) ||
!std::isfinite(current_jacobian) || !(current_jacobian > 0.0)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-geometry", source_location_, identity_,
"Inertia requires positive reference and current mass-point "
"Jacobians.");
}
for (std::size_t row = 0U; row < kDofs; ++row) {
velocity_point = velocity_point + h[row] * velocity[row];
acceleration_point = acceleration_point + h[row] * acceleration[row];
}
acceleration_point = acceleration_point + convective_point;
const double scale = density_ * weight * jacobian;
if (!std::isfinite(scale) || !(scale > 0.0)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-positive-mass", source_location_,
identity_,
"Mass-point integration weight must be finite and positive.");
}
result.kinetic_energy +=
0.5 * scale * velocity_point.Dot(velocity_point);
result.momentum = result.momentum + scale * velocity_point;
result.momentum_rate =
result.momentum_rate + scale * acceleration_point;
result.angular_momentum =
result.angular_momentum + scale * position.Cross(velocity_point);
result.angular_momentum_rate =
result.angular_momentum_rate +
scale * position.Cross(acceleration_point);
result.momentum_rate_scale += scale * acceleration_point.Norm();
result.angular_momentum_rate_scale +=
scale * position.Cross(acceleration_point).Norm();
for (std::size_t row = 0U; row < kDofs; ++row) {
result.force[row] += scale * h[row].Dot(acceleration_point);
// Keep c independent of a; subtracting M*a destroys small c values.
result.convective_force[row] += scale * h[row].Dot(convective_point);
for (std::size_t column = 0U; column < kDofs; ++column) {
result.mass(row, column) += scale * h[row].Dot(h[column]);
result.velocity_tangent(row, column) +=
scale * h[row].Dot(velocity_derivative[column]);
double position_entry = h[row].Dot(position_derivative[column]);
if (row / 6U == column / 6U && row % 6U >= 3U &&
column % 6U >= 3U) {
const std::size_t node = row / 6U;
position_entry +=
z * shape.values[node] *
rotations[node]
.director_second[row % 6U - 3U][column % 6U - 3U]
.Dot(acceleration_point);
}
result.position_tangent(row, column) += scale * position_entry;
}
}
}
}
}
if (!IsFinite(result.mass) || !IsFinite(result.position_tangent) ||
!IsFinite(result.velocity_tangent) || !IsFinite(result.force) ||
!IsFinite(result.convective_force) ||
!std::isfinite(result.kinetic_energy) || !result.momentum.IsFinite() ||
!result.angular_momentum.IsFinite() || !result.momentum_rate.IsFinite() ||
!result.angular_momentum_rate.IsFinite() ||
!std::isfinite(result.momentum_rate_scale) ||
!std::isfinite(result.angular_momentum_rate_scale)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-evaluation", source_location_, identity_,
"Nonlinear MITC4 inertia evaluation must remain finite.");
}
const Status physical_mass = CheckPhysicalMass(result.mass, nulls, length,
source_location_, identity_);
if (!physical_mass.IsOk()) {
return Result<NonlinearMitc4InertiaEvaluation>::Failure(physical_mass);
}
return Result<NonlinearMitc4InertiaEvaluation>::Success(std::move(result));
}
} // namespace fesa
+316 -111
View File
@@ -42,9 +42,8 @@ template <class T>
Result<T> Failure(const std::string& code, const SourceLocation& location,
const std::string& identity, std::string message) {
return Result<T>::Failure(Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, "*ELEMENT", identity,
std::move(message)}}));
FailureCategory::kModel, {{Severity::kError, code, location, "*ELEMENT",
identity, std::move(message)}}));
}
/// @brief Builds the stable source label concatenation used by diagnostics.
@@ -134,8 +133,7 @@ bool MakeLocalFrame(const Vector3& midsurface_xi, const Vector3& midsurface_eta,
}
const Vector3 e1_candidate = midsurface_xi - midsurface_xi.Dot(e3) * e3;
const double e1_norm = e1_candidate.Norm();
if (!e1_candidate.IsFinite() || !std::isfinite(e1_norm) ||
!(e1_norm > 0.0)) {
if (!e1_candidate.IsFinite() || !std::isfinite(e1_norm) || !(e1_norm > 0.0)) {
return false;
}
const Vector3 e1 = e1_candidate / e1_norm;
@@ -155,8 +153,7 @@ bool EvaluateGeometry(const std::array<Vector3, kNodeCount>& coordinates,
const double thickness, const double xi, const double eta,
const double zeta, Geometry& geometry) {
const Mitc4ShapeFunctions shape = Mitc4Shell::ShapeFunctions(xi, eta);
const Vector3 reference_xi =
DerivativeSum(shape.xi_derivatives, coordinates);
const Vector3 reference_xi = DerivativeSum(shape.xi_derivatives, coordinates);
const Vector3 reference_eta =
DerivativeSum(shape.eta_derivatives, coordinates);
const Vector3 initial_director_xi =
@@ -218,11 +215,10 @@ Tensor DirectStrain(const Geometry& geometry) {
Tensor strain{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
strain[row][column] =
0.5 * (geometry.current_covariant[row].Dot(
geometry.current_covariant[column]) -
geometry.reference_covariant[row].Dot(
geometry.reference_covariant[column]));
strain[row][column] = 0.5 * (geometry.current_covariant[row].Dot(
geometry.current_covariant[column]) -
geometry.reference_covariant[row].Dot(
geometry.reference_covariant[column]));
}
}
strain[2U][2U] = 0.0;
@@ -278,17 +274,17 @@ EngineeringStress Stress(const EngineeringStrain& strain, const double youngs,
const double shear = kShearCorrection * youngs / (2.0 * (1.0 + poisson));
return {factor * (strain[0U] + poisson * strain[1U]),
factor * (poisson * strain[0U] + strain[1U]),
factor * (1.0 - poisson) * 0.5 * strain[2U],
shear * strain[3U], shear * strain[4U]};
factor * (1.0 - poisson) * 0.5 * strain[2U], shear * strain[3U],
shear * strain[4U]};
}
/// @brief Evaluates direct strain at one mandatory edge-midpoint tying point.
bool EvaluateTyingSample(const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const std::array<Vector3, kNodeCount>& directors,
const NonlinearMitc4TrialDofs& trial,
const double thickness, const double xi,
const double eta, Tensor& strain) {
bool EvaluateTyingSample(
const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const std::array<Vector3, kNodeCount>& directors,
const NonlinearMitc4TrialDofs& trial, const double thickness,
const double xi, const double eta, Tensor& strain) {
Geometry geometry{};
if (!EvaluateGeometry(coordinates, initial_directors, directors, trial,
thickness, xi, eta, 0.0, geometry)) {
@@ -314,12 +310,13 @@ bool EvaluatePoint(const std::array<Vector3, kNodeCount>& coordinates,
Tensor covariant = DirectStrain(point.geometry);
const Mitc4TyingWeights weights = Mitc4Shell::TyingWeights(xi, eta);
covariant[0U][2U] = weights.xi_zeta[0U] * tying_samples[0U][0U][2U] +
weights.xi_zeta[1U] * tying_samples[1U][0U][2U];
weights.xi_zeta[1U] * tying_samples[1U][0U][2U];
covariant[2U][0U] = covariant[0U][2U];
covariant[1U][2U] = weights.eta_zeta[0U] * tying_samples[2U][1U][2U] +
weights.eta_zeta[1U] * tying_samples[3U][1U][2U];
weights.eta_zeta[1U] * tying_samples[3U][1U][2U];
covariant[2U][1U] = covariant[1U][2U];
const Tensor cartesian = CartesianStrain(covariant, point.geometry.reciprocal);
const Tensor cartesian =
CartesianStrain(covariant, point.geometry.reciprocal);
point.strain = LocalEngineering(cartesian, point.geometry.local_frame);
point.stress = Stress(point.strain, youngs, poisson);
return IsFinite(covariant) && IsFinite(cartesian) && IsFinite(point.strain) &&
@@ -372,6 +369,10 @@ struct Jet {
double value{};
std::array<double, kGlobalDofCount> first{};
Hessian second;
double directional_first{};
double directional_second{};
std::array<double, kGlobalDofCount> mixed_first{};
std::array<double, kGlobalDofCount> mixed_second{};
};
using JetVector3 = std::array<Jet, 3>;
@@ -386,9 +387,11 @@ Jet Constant(const double value) {
}
/// @brief Returns one independent global trial coordinate.
Jet Variable(const double value, const std::size_t index) {
Jet Variable(const double value, const std::size_t index,
const double direction = 0.0) {
Jet result = Constant(value);
result.first[index] = 1.0;
result.directional_first = direction;
return result;
}
@@ -396,8 +399,15 @@ Jet Variable(const double value, const std::size_t index) {
Jet operator+(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value + right.value;
result.directional_first = left.directional_first + right.directional_first;
result.directional_second =
left.directional_second + right.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = left.first[index] + right.first[index];
result.mixed_first[index] =
left.mixed_first[index] + right.mixed_first[index];
result.mixed_second[index] =
left.mixed_second[index] + right.mixed_second[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = left.second[index] + right.second[index];
@@ -409,8 +419,15 @@ Jet operator+(const Jet& left, const Jet& right) {
Jet operator-(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value - right.value;
result.directional_first = left.directional_first - right.directional_first;
result.directional_second =
left.directional_second - right.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = left.first[index] - right.first[index];
result.mixed_first[index] =
left.mixed_first[index] - right.mixed_first[index];
result.mixed_second[index] =
left.mixed_second[index] - right.mixed_second[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = left.second[index] - right.second[index];
@@ -422,9 +439,26 @@ Jet operator-(const Jet& left, const Jet& right) {
Jet operator*(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value * right.value;
result.directional_first = left.directional_first * right.value +
left.value * right.directional_first;
result.directional_second =
left.directional_second * right.value +
2.0 * left.directional_first * right.directional_first +
left.value * right.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] =
left.first[index] * right.value + left.value * right.first[index];
result.mixed_first[index] = left.mixed_first[index] * right.value +
left.first[index] * right.directional_first +
left.directional_first * right.first[index] +
left.value * right.mixed_first[index];
result.mixed_second[index] =
left.mixed_second[index] * right.value +
2.0 * left.mixed_first[index] * right.directional_first +
left.first[index] * right.directional_second +
left.directional_second * right.first[index] +
2.0 * left.directional_first * right.mixed_first[index] +
left.value * right.mixed_second[index];
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
@@ -442,8 +476,12 @@ Jet operator*(const Jet& left, const Jet& right) {
Jet operator*(const double scalar, const Jet& value) {
Jet result{};
result.value = scalar * value.value;
result.directional_first = scalar * value.directional_first;
result.directional_second = scalar * value.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = scalar * value.first[index];
result.mixed_first[index] = scalar * value.mixed_first[index];
result.mixed_second[index] = scalar * value.mixed_second[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = scalar * value.second[index];
@@ -456,11 +494,24 @@ Jet operator*(const Jet& value, const double scalar) { return scalar * value; }
/// @brief Applies a scalar unary function through its first two derivatives.
Jet Compose(const Jet& input, const double value, const double first,
const double second) {
const double second, const double third) {
Jet result{};
result.value = value;
result.directional_first = first * input.directional_first;
result.directional_second =
second * input.directional_first * input.directional_first +
first * input.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = first * input.first[index];
result.mixed_first[index] =
second * input.first[index] * input.directional_first +
first * input.mixed_first[index];
result.mixed_second[index] =
third * input.first[index] * input.directional_first *
input.directional_first +
second * (2.0 * input.mixed_first[index] * input.directional_first +
input.first[index] * input.directional_second) +
first * input.mixed_second[index];
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
@@ -476,7 +527,8 @@ Jet Compose(const Jet& input, const double value, const double first,
Jet Reciprocal(const Jet& value) {
const double inverse = 1.0 / value.value;
return Compose(value, inverse, -inverse * inverse,
2.0 * inverse * inverse * inverse);
2.0 * inverse * inverse * inverse,
-6.0 * inverse * inverse * inverse * inverse);
}
/// @brief Divides two second-order trial-coordinate quantities.
@@ -487,19 +539,20 @@ Jet operator/(const Jet& left, const Jet& right) {
/// @brief Computes the analytic square root variation.
Jet Sqrt(const Jet& value) {
const double root = std::sqrt(value.value);
return Compose(value, root, 0.5 / root, -0.25 / (root * root * root));
return Compose(value, root, 0.5 / root, -0.25 / (root * root * root),
0.375 / (root * root * root * root * root));
}
/// @brief Computes the analytic sine variation.
Jet Sin(const Jet& value) {
return Compose(value, std::sin(value.value), std::cos(value.value),
-std::sin(value.value));
-std::sin(value.value), -std::cos(value.value));
}
/// @brief Computes the analytic cosine variation.
Jet Cos(const Jet& value) {
return Compose(value, std::cos(value.value), -std::sin(value.value),
-std::cos(value.value));
-std::cos(value.value), std::sin(value.value));
}
/// @brief Computes the two-argument angle and its analytic first/second map.
@@ -529,15 +582,40 @@ Jet Atan2(const Jet& y, const Jet& x) {
derivative_xx * x.first[row] * x.first[column];
}
}
// Use a bounded ratio for the directional third derivative only. Retain
// the established value/gradient/Hessian reduction above for static runs.
const bool use_x = std::abs(x.value) >= std::abs(y.value);
const Jet ratio = use_x ? y / x : x / y;
const double denominator = 1.0 + ratio.value * ratio.value;
const Jet angle = Compose(ratio, std::atan(ratio.value), 1.0 / denominator,
-2.0 * ratio.value / (denominator * denominator),
(6.0 * ratio.value * ratio.value - 2.0) /
(denominator * denominator * denominator));
const double sign = use_x ? 1.0 : -1.0;
result.directional_first = sign * angle.directional_first;
result.directional_second = sign * angle.directional_second;
for (std::size_t i = 0U; i < kGlobalDofCount; ++i) {
result.mixed_first[i] = sign * angle.mixed_first[i];
result.mixed_second[i] = sign * angle.mixed_second[i];
}
return result;
}
/// @brief Checks every coefficient in an automatic-differentiation quantity.
bool IsFinite(const Jet& value) {
if (!std::isfinite(value.value)) {
if (!std::isfinite(value.value) || !std::isfinite(value.directional_first) ||
!std::isfinite(value.directional_second)) {
return false;
}
return std::all_of(value.first.begin(), value.first.end(),
return std::all_of(value.mixed_first.begin(), value.mixed_first.end(),
[](const double coefficient) {
return std::isfinite(coefficient);
}) &&
std::all_of(value.mixed_second.begin(), value.mixed_second.end(),
[](const double coefficient) {
return std::isfinite(coefficient);
}) &&
std::all_of(value.first.begin(), value.first.end(),
[](const double coefficient) {
return std::isfinite(coefficient);
}) &&
@@ -728,8 +806,39 @@ JetMatrix3 ExpSkew(const JetVector3& rotation) {
(Constant(1.0) - Cos(norm)) / squared_norm;
}
const JetMatrix3 skew = Skew(rotation);
return Add(Add(Identity(), Scale(sine_over_norm, skew)),
Scale(one_minus_cosine_over_norm_squared, Multiply(skew, skew)));
JetMatrix3 result =
Add(Add(Identity(), Scale(sine_over_norm, skew)),
Scale(one_minus_cosine_over_norm_squared, Multiply(skew, skew)));
if (squared_norm.value < 0.25) {
// Third derivatives amplify cancellation in 1-cos(theta). Preserve the
// established static coefficients and use a stable series for the new
// directional derivatives of the complete rotation tensor.
Jet sine_series = Constant(1.0);
Jet cosine_series = Constant(0.5);
Jet power = Constant(1.0);
double sine_coefficient = 1.0;
double cosine_coefficient = 0.5;
for (unsigned int k = 1U; k <= 9U; ++k) {
power = power * squared_norm;
sine_coefficient /= -static_cast<double>((2U * k) * (2U * k + 1U));
cosine_coefficient /= -static_cast<double>((2U * k + 1U) * (2U * k + 2U));
sine_series = sine_series + sine_coefficient * power;
cosine_series = cosine_series + cosine_coefficient * power;
}
const JetMatrix3 stable = Add(Add(Identity(), Scale(sine_series, skew)),
Scale(cosine_series, Multiply(skew, skew)));
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column].directional_first =
stable[row][column].directional_first;
result[row][column].directional_second =
stable[row][column].directional_second;
result[row][column].mixed_first = stable[row][column].mixed_first;
result[row][column].mixed_second = stable[row][column].mixed_second;
}
}
}
return result;
}
/// @brief Stores the complete Jet-valued global element trial coordinates.
@@ -741,14 +850,18 @@ struct JetTrial {
/// @brief Lifts one validated nonlinear trial into independent Jet variables.
JetTrial MakeJetTrial(const NonlinearMitc4TrialDofs& trial,
const std::array<Matrix, kNodeCount>& initial_frames) {
const std::array<Matrix, kNodeCount>& initial_frames,
const Vector* direction) {
JetTrial result{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
result.displacement[node][component] =
Variable(trial.displacement[node][component], 6U * node + component);
result.displacement[node][component] = Variable(
trial.displacement[node][component], 6U * node + component,
direction == nullptr ? 0.0 : (*direction)[6U * node + component]);
result.rotation[node][component] = Variable(
trial.rotation_vector[node][component], 6U * node + 3U + component);
trial.rotation_vector[node][component], 6U * node + 3U + component,
direction == nullptr ? 0.0
: (*direction)[6U * node + 3U + component]);
}
JetMatrix3 frame{};
for (std::size_t row = 0U; row < 3U; ++row) {
@@ -764,52 +877,42 @@ JetTrial MakeJetTrial(const NonlinearMitc4TrialDofs& trial,
return result;
}
/// @brief Forms one current covariant basis from the exact trial variables.
std::array<JetVector3, 3> CurrentCovariant(
const std::array<Vector3, kNodeCount>& coordinates, const JetTrial& trial,
const double thickness, const double xi, const double eta,
const double zeta) {
const Mitc4ShapeFunctions shape = Mitc4Shell::ShapeFunctions(xi, eta);
JetVector3 displacement_xi{};
JetVector3 displacement_eta{};
JetVector3 director{};
JetVector3 director_xi{};
JetVector3 director_eta{};
Vector3 reference_xi{};
Vector3 reference_eta{};
/// @brief Forms covariant increments directly, preserving exact zero motion.
std::array<JetVector3, 3> CovariantIncrement(
const std::array<Vector3, kNodeCount>& initial_directors,
const JetTrial& trial, const double thickness, const double xi,
const double eta, const double zeta) {
const auto shape = Mitc4Shell::ShapeFunctions(xi, eta);
std::array<JetVector3, 3> increment{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
reference_xi =
reference_xi + shape.xi_derivatives[node] * coordinates[node];
reference_eta =
reference_eta + shape.eta_derivatives[node] * coordinates[node];
displacement_xi = Add(displacement_xi, Scale(shape.xi_derivatives[node],
trial.displacement[node]));
displacement_eta = Add(displacement_eta, Scale(shape.eta_derivatives[node],
trial.displacement[node]));
director = Add(director, Scale(shape.values[node], trial.director[node]));
director_xi = Add(director_xi,
Scale(shape.xi_derivatives[node], trial.director[node]));
director_eta = Add(
director_eta, Scale(shape.eta_derivatives[node], trial.director[node]));
const auto displacement =
Subtract(trial.displacement[node], trial.displacement[0U]);
const auto director =
Subtract(trial.director[node], ConstantVector(initial_directors[node]));
const auto layer =
Add(displacement, Scale(0.5 * thickness * zeta, director));
increment[0U] =
Add(increment[0U], Scale(shape.xi_derivatives[node], layer));
increment[1U] =
Add(increment[1U], Scale(shape.eta_derivatives[node], layer));
increment[2U] = Add(increment[2U],
Scale(0.5 * thickness * shape.values[node], director));
}
const double half_thickness = 0.5 * thickness;
return {Add(Add(ConstantVector(reference_xi), displacement_xi),
Scale(half_thickness * zeta, director_xi)),
Add(Add(ConstantVector(reference_eta), displacement_eta),
Scale(half_thickness * zeta, director_eta)),
Scale(half_thickness, director)};
return increment;
}
/// @brief Forms the direct covariant Green--Lagrange strain tensor.
JetTensor DirectStrain(const std::array<JetVector3, 3>& current,
JetTensor DirectStrain(const std::array<JetVector3, 3>& increment,
const Geometry& reference) {
JetTensor result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] =
0.5 * (Dot(current[row], current[column]) -
Constant(reference.reference_covariant[row].Dot(
reference.reference_covariant[column])));
0.5 * (Dot(ConstantVector(reference.reference_covariant[row]),
increment[column]) +
Dot(increment[row],
ConstantVector(reference.reference_covariant[column])) +
Dot(increment[row], increment[column]));
}
}
result[2U][2U] = Constant(0.0);
@@ -867,9 +970,9 @@ Jet PhysicalEnergy(const std::array<Vector3, kNodeCount>& coordinates,
return {};
}
tying[point] =
DirectStrain(CurrentCovariant(coordinates, trial, thickness,
tying_locations[point][0U],
tying_locations[point][1U], 0.0),
DirectStrain(CovariantIncrement(initial_directors, trial, thickness,
tying_locations[point][0U],
tying_locations[point][1U], 0.0),
reference);
}
@@ -892,8 +995,8 @@ Jet PhysicalEnergy(const std::array<Vector3, kNodeCount>& coordinates,
return {};
}
JetTensor covariant =
DirectStrain(CurrentCovariant(coordinates, trial, thickness,
surface[0U], surface[1U], zeta),
DirectStrain(CovariantIncrement(initial_directors, trial, thickness,
surface[0U], surface[1U], zeta),
reference);
const Mitc4TyingWeights weights =
Mitc4Shell::TyingWeights(surface[0U], surface[1U]);
@@ -940,11 +1043,41 @@ JetMatrix3 PolarRotation(const JetMatrix3& deformation) {
return result;
}
/// @brief Returns the speed of physical spin for a unit chart null vector.
Jet DrillSpinRate(const JetVector3& phi, const Vector3& director) {
const Jet square = Dot(phi, phi);
Jet coefficient{};
if (square.value < 0.25) {
constexpr std::array<double, 9> series{1.0 / 12.0,
1.0 / 720.0,
1.0 / 30240.0,
1.0 / 1209600.0,
1.0 / 47900160.0,
691.0 / 1307674368000.0,
1.0 / 74724249600.0,
3617.0 / 10670622842880000.0,
43867.0 / 5109094217170944000.0};
coefficient = Constant(series.back());
for (std::size_t i = series.size() - 1U; i > 0U; --i)
coefficient = Constant(series[i - 1U]) + square * coefficient;
} else {
const Jet half = 0.5 * Sqrt(square);
coefficient = (Constant(1.0) - half * Cos(half) * Reciprocal(Sin(half))) *
Reciprocal(square);
}
const auto d0 = ConstantVector(director);
const auto cross = Cross(phi, d0);
const auto inverse_action =
Add(Add(d0, Scale(0.5, cross)), Scale(coefficient, Cross(phi, cross)));
return Reciprocal(Sqrt(Dot(inverse_action, inverse_action)));
}
/// @brief Evaluates the objective numerical drilling potential and variations.
Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const JetTrial& trial, const double thickness,
const double drilling_stiffness, bool& valid) {
const double drilling_stiffness, bool& valid, Matrix* metric,
std::array<Jet, 4>* constraints) {
const NonlinearMitc4TrialDofs reference_trial{};
Geometry reference{};
if (!EvaluateGeometry(coordinates, initial_directors, initial_directors,
@@ -953,8 +1086,8 @@ Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
return {};
}
const auto current =
CurrentCovariant(coordinates, trial, thickness, 0.0, 0.0, 0.0);
JetMatrix3 deformation{};
CovariantIncrement(initial_directors, trial, thickness, 0.0, 0.0, 0.0);
JetMatrix3 deformation = Identity();
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
for (std::size_t alpha = 0U; alpha < 3U; ++alpha) {
@@ -991,7 +1124,19 @@ Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
const Jet cosine =
0.5 * (twist[0U][0U] + twist[1U][1U] + twist[2U][2U] - Constant(1.0));
const Jet gamma = Atan2(sine, cosine);
if (constraints != nullptr) {
// n^T grad(Uphys)=0 and Dgamma_j[n_i]=delta_ij/||Jl^-1 d||.
(*constraints)[node] =
drilling_stiffness *
DrillSpinRate(trial.rotation[node], initial_directors[node]) * gamma;
}
energy = energy + 0.5 * drilling_stiffness * gamma * gamma;
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
(*metric)(row, column) +=
drilling_stiffness * gamma.first[row] * gamma.first[column];
}
}
}
if (!IsFinite(energy)) {
valid = false;
@@ -1040,13 +1185,15 @@ Result<NonlinearMitc4Shell> NonlinearMitc4Shell::Create(
for (std::size_t previous = 0U; previous < node; ++previous) {
if (coordinates[node] == coordinates[previous]) {
return Failure<NonlinearMitc4Shell>(
"invalid-nonlinear-shell-geometry", nodes[node]->location,
identity, "Nonlinear MITC4 nodes must have distinct coordinates.");
"invalid-nonlinear-shell-geometry", nodes[node]->location, identity,
"Nonlinear MITC4 nodes must have distinct coordinates.");
}
if (initial_frames[node].node_index == initial_frames[previous].node_index) {
if (initial_frames[node].node_index ==
initial_frames[previous].node_index) {
return Failure<NonlinearMitc4Shell>(
"invalid-nonlinear-shell-frame", nodes[node]->location, identity,
"Nonlinear MITC4 initial frames must identify four distinct nodes.");
"Nonlinear MITC4 initial frames must identify four distinct "
"nodes.");
}
}
}
@@ -1089,7 +1236,8 @@ Result<NonlinearMitc4Shell> NonlinearMitc4Shell::Create(
})) {
return Failure<NonlinearMitc4Shell>(
"invalid-nonlinear-shell-frame", nodes[0U]->location, identity,
"Nonlinear MITC4 initial directors must follow the source positive face.");
"Nonlinear MITC4 initial directors must follow the source positive "
"face.");
}
if (!std::isfinite(section.thickness) || !(section.thickness > 0.0)) {
@@ -1125,14 +1273,14 @@ Result<NonlinearMitc4Shell> NonlinearMitc4Shell::Create(
}
NonlinearMitc4Shell shell{coordinates,
std::move(frames),
directors,
section.thickness,
material.youngs_modulus,
material.poisson_ratio,
linear_stiffness.Value().drilling_stiffness,
nodes[0U]->location,
identity};
std::move(frames),
directors,
section.thickness,
material.youngs_modulus,
material.poisson_ratio,
linear_stiffness.Value().drilling_stiffness,
nodes[0U]->location,
identity};
const auto reference = shell.EvaluateKinematics(NonlinearMitc4TrialDofs{});
if (!reference.HasValue()) {
return Result<NonlinearMitc4Shell>::Failure(reference.GetStatus());
@@ -1150,12 +1298,13 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
"invalid-nonlinear-shell-trial", source_location_, identity_,
"Nonlinear MITC4 trial displacements and rotations must be finite.");
}
const auto state =
MakeFiniteRotationState(trial.rotation_vector[node], initial_frames_[node]);
const auto state = MakeFiniteRotationState(trial.rotation_vector[node],
initial_frames_[node]);
if (!state.HasValue()) {
return Failure<NonlinearMitc4Recovery>(
"invalid-nonlinear-shell-rotation", source_location_, identity_,
"Nonlinear MITC4 trial rotations must remain in the principal chart.");
"Nonlinear MITC4 trial rotations must remain in the principal "
"chart.");
}
directors[node] = state.Value().director;
}
@@ -1185,10 +1334,12 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
for (std::size_t point = 0U; point < tying_locations.size(); ++point) {
if (!EvaluateTyingSample(coordinates_, initial_directors_, directors, trial,
thickness_, tying_locations[point][0U],
tying_locations[point][1U], tying_samples[point])) {
tying_locations[point][1U],
tying_samples[point])) {
return Failure<NonlinearMitc4Recovery>(
"invalid-nonlinear-shell-geometry", source_location_, identity_,
"Nonlinear MITC4 tying-point reference or trial geometry is invalid.");
"Nonlinear MITC4 tying-point reference or trial geometry is "
"invalid.");
}
}
@@ -1198,8 +1349,7 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4};
const std::array<std::array<double, 2>, 4> surface_points{
std::array<double, 2>{-gauss, -gauss},
std::array<double, 2>{gauss, -gauss},
std::array<double, 2>{gauss, gauss},
std::array<double, 2>{gauss, -gauss}, std::array<double, 2>{gauss, gauss},
std::array<double, 2>{-gauss, gauss}};
constexpr std::array<double, 2> thickness_gauss{-1.0, 1.0};
constexpr std::array<double, 3> stress_zeta{-1.0, 0.0, 1.0};
@@ -1236,12 +1386,12 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
for (std::size_t component = 0U; component < 3U; ++component) {
row.generalized_strain[component] += 0.5 * point.strain[component];
row.generalized_strain[3U + component] +=
3.0 * thickness_sign * gauss * point.strain[component] /
thickness_;
3.0 * thickness_sign * gauss * point.strain[component] / thickness_;
row.section_resultant[component] +=
0.5 * thickness_ * point.stress[component];
row.section_resultant[3U + component] +=
0.25 * thickness_ * thickness_sign * gauss * point.stress[component];
row.section_resultant[3U + component] += 0.25 * thickness_ *
thickness_sign * gauss *
point.stress[component];
}
for (std::size_t component = 0U; component < 2U; ++component) {
row.generalized_strain[6U + component] +=
@@ -1269,9 +1419,10 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
"invalid-nonlinear-shell-geometry", source_location_, identity_,
"Nonlinear MITC4 section-position recovery geometry is invalid.");
}
row.stress[position] = {stress_position[position], stress_zeta[position],
{point.stress[0U], point.stress[1U],
point.stress[2U]}};
row.stress[position] = {
stress_position[position],
stress_zeta[position],
{point.stress[0U], point.stress[1U], point.stress[2U]}};
}
}
@@ -1289,7 +1440,22 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::RecoverState(
}
Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial) {
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial,
const Vector* coordinate_direction) {
if (coordinate_direction != nullptr) {
if (coordinate_direction->Size() != kGlobalDofCount) {
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_, "The directional derivative requires 24 DOFs.");
}
for (std::size_t i = 0U; i < kGlobalDofCount; ++i) {
if (!std::isfinite((*coordinate_direction)[i])) {
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_, "The directional derivative must be finite.");
}
}
}
const auto recovery = element.EvaluateKinematics(trial);
if (!recovery.HasValue()) {
return Result<NonlinearMitc4InternalEquilibrium>::Failure(
@@ -1297,14 +1463,18 @@ Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
}
bool valid = true;
const JetTrial jet_trial = MakeJetTrial(trial, element.initial_frames_);
const JetTrial jet_trial =
MakeJetTrial(trial, element.initial_frames_, coordinate_direction);
const Jet physical =
PhysicalEnergy(element.coordinates_, element.initial_directors_,
jet_trial, element.thickness_, element.youngs_modulus_,
element.poisson_ratio_, valid);
Matrix drilling_metric{kGlobalDofCount, kGlobalDofCount};
std::array<Jet, 4> constraints{};
const Jet drilling = DrillingEnergy(
element.coordinates_, element.initial_directors_, jet_trial,
element.thickness_, element.drilling_stiffness_, valid);
element.thickness_, element.drilling_stiffness_, valid, &drilling_metric,
coordinate_direction != nullptr ? &constraints : nullptr);
const Jet internal_energy = physical + drilling;
if (!valid || !IsFinite(internal_energy)) {
return Failure<NonlinearMitc4InternalEquilibrium>(
@@ -1344,8 +1514,43 @@ Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
std::move(internal_residual), std::move(internal_tangent),
Vector{kGlobalDofCount}, recovery.Value(),
recovery.Value().physical_strain_energy};
if (coordinate_direction != nullptr) {
equilibrium.drilling_constraint = Vector{4U};
equilibrium.drilling_constraint_gradient = Matrix{4U, kGlobalDofCount};
for (std::size_t node = 0U; node < 4U; ++node) {
if (!IsFinite(constraints[node]))
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_,
"Drilling constraint derivatives must remain finite.");
equilibrium.drilling_constraint[node] = constraints[node].value;
equilibrium.drilling_constraint_hessian[node] =
Matrix{kGlobalDofCount, kGlobalDofCount};
for (std::size_t i = 0U; i < kGlobalDofCount; ++i) {
equilibrium.drilling_constraint_gradient(node, i) =
constraints[node].first[i];
for (std::size_t j = 0U; j < kGlobalDofCount; ++j)
equilibrium.drilling_constraint_hessian[node](i, j) =
constraints[node].second[i * kGlobalDofCount + j];
}
}
}
equilibrium.physical_force = Vector{kGlobalDofCount};
equilibrium.drilling_force = Vector{kGlobalDofCount};
equilibrium.drilling_tangent = Matrix{kGlobalDofCount, kGlobalDofCount};
equilibrium.drilling_metric = std::move(drilling_metric);
equilibrium.drilling_energy = drilling.value;
equilibrium.force_directional_second = Vector{kGlobalDofCount};
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
equilibrium.internal_force[index] = equilibrium.internal_residual[index];
equilibrium.physical_force[index] = physical.first[index];
equilibrium.drilling_force[index] = drilling.first[index];
equilibrium.force_directional_second[index] =
internal_energy.mixed_second[index];
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
equilibrium.drilling_tangent(index, column) =
drilling.second[index * kGlobalDofCount + column];
}
}
return Result<NonlinearMitc4InternalEquilibrium>::Success(
std::move(equilibrium));
File diff suppressed because it is too large Load Diff