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