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