feat(hex8): integrate paired HHT dynamic state and bounded verification
This commit is contained in:
@@ -17,7 +17,9 @@ add_library(
|
||||
analysis/nonlinear_static_control.cpp
|
||||
analysis/nonlinear_static_procedure.cpp
|
||||
analysis/prepared_analysis.cpp
|
||||
analysis/solid_dynamic_analysis_state.cpp
|
||||
analysis/solid_frequency_procedure.cpp
|
||||
analysis/solid_hht_dynamic_procedure.cpp
|
||||
analysis/solid_static_procedure.cpp
|
||||
analysis/solid_static_state.cpp
|
||||
analysis/thermal_analysis_state.cpp
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/hex8_element.h"
|
||||
#include "math/high_low_arithmetic.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
/// @brief Rejects a candidate without changing observable history.
|
||||
Status Failure(const std::string& message) {
|
||||
return Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError,
|
||||
"solid-dynamic-invalid-state",
|
||||
{},
|
||||
"DYNAMIC",
|
||||
{},
|
||||
message}});
|
||||
}
|
||||
/// @brief Checks a physical record without replacing nonfinite quantities.
|
||||
template <std::size_t N>
|
||||
bool Finite(const std::array<double, N>& values) {
|
||||
return std::all_of(values.begin(), values.end(),
|
||||
[](double x) { return std::isfinite(x); });
|
||||
}
|
||||
bool ValidPair(double high, double low) {
|
||||
const math_internal::HighLow value(high, low);
|
||||
return math_internal::IsFinite(value) && math_internal::IsNormalized(value);
|
||||
}
|
||||
bool ValidPair(const Vector& high, const Vector& low, std::size_t size) {
|
||||
if (high.Size() != size || low.Size() != size) return false;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
if (!ValidPair(high[i], low[i])) return false;
|
||||
return true;
|
||||
}
|
||||
template <std::size_t N>
|
||||
bool ValidPair(const std::array<double, N>& high,
|
||||
const std::array<double, N>& low) {
|
||||
for (std::size_t i = 0; i < N; ++i)
|
||||
if (!ValidPair(high[i], low[i])) return false;
|
||||
return true;
|
||||
}
|
||||
/// @brief Preserves canonical and raw source identity independently of indices.
|
||||
bool SameId(const SourceEntityId& a, const SourceEntityId& b) {
|
||||
return a.instance_name == b.instance_name &&
|
||||
a.source_label == b.source_label &&
|
||||
a.source_label_text == b.source_label_text;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Result<SolidDynamicAnalysisState> SolidDynamicAnalysisState::Create(
|
||||
const Domain& domain, const DofManager& dofs) {
|
||||
const auto* step = domain.Dynamic();
|
||||
if (!domain.IsSolid() || step == nullptr ||
|
||||
dofs.Profile() != DofProfile::kSolidStructural ||
|
||||
dofs.FullDofCount() != 3 * domain.Nodes().size() ||
|
||||
!dofs.ValidateInvariants().IsOk() || step->increment_count == 0 ||
|
||||
!std::isfinite(step->initial_increment) || step->initial_increment <= 0 ||
|
||||
!std::isfinite(step->time_period) ||
|
||||
static_cast<double>(step->increment_count) * step->initial_increment !=
|
||||
step->time_period ||
|
||||
step->increment_count > step->maximum_increments) {
|
||||
return Result<SolidDynamicAnalysisState>::Failure(
|
||||
Failure("Solid HHT requires a valid 3N exact fixed schedule."));
|
||||
}
|
||||
for (std::size_t i = 0; i < dofs.PrescribedValues().Size(); ++i)
|
||||
if (dofs.PrescribedValues()[i] != 0)
|
||||
return Result<SolidDynamicAnalysisState>::Failure(
|
||||
Failure("Dynamic prescribed translations must be exactly zero."));
|
||||
SolidDynamicAnalysisState state;
|
||||
state.full_dof_count_ = dofs.FullDofCount();
|
||||
state.increment_count_ = step->increment_count;
|
||||
state.time_increment_ = step->initial_increment;
|
||||
state.constrained_dofs_ = dofs.ConstrainedDofs();
|
||||
for (const auto& node : domain.Nodes())
|
||||
state.source_ids_.push_back(node.source_id);
|
||||
ElementFactory factory;
|
||||
for (std::size_t e = 0; e < domain.Elements().Size(); ++e) {
|
||||
auto runtime = factory.Create(domain.Elements()[e], domain);
|
||||
if (!runtime.HasValue())
|
||||
return Result<SolidDynamicAnalysisState>::Failure(runtime.GetStatus());
|
||||
const auto* solid = dynamic_cast<const Hex8Element*>(runtime.Value().get());
|
||||
if (solid == nullptr)
|
||||
return Result<SolidDynamicAnalysisState>::Failure(Failure(
|
||||
"Dynamic Solid inventory contains a different element family."));
|
||||
auto zero = solid->EvaluateLinear(Vector(24));
|
||||
if (!zero.HasValue())
|
||||
return Result<SolidDynamicAnalysisState>::Failure(zero.GetStatus());
|
||||
state.point_inventory_.insert(state.point_inventory_.end(),
|
||||
zero.Value().points.begin(),
|
||||
zero.Value().points.end());
|
||||
}
|
||||
state.initialized_ = true;
|
||||
return Result<SolidDynamicAnalysisState>::Success(std::move(state));
|
||||
}
|
||||
|
||||
Status SolidDynamicAnalysisState::CommitAcceptedFrame(
|
||||
const SolidDynamicAcceptedFrameState& f) {
|
||||
if (!math_internal::ArithmeticEnvironmentSupported())
|
||||
return Failure(
|
||||
"Paired state commit requires nearest rounding and gradual "
|
||||
"underflow.");
|
||||
const std::size_t index = accepted_frames_.size();
|
||||
if (!initialized_ || index > increment_count_ || f.frame_index != index ||
|
||||
f.increment_index != index || f.is_initial != (index == 0) ||
|
||||
f.time != static_cast<double>(index) * time_increment_ ||
|
||||
f.iteration_count > 100 || (index == 0 && f.iteration_count != 0) ||
|
||||
f.nodal_internal_order.size() != source_ids_.size() ||
|
||||
f.nodal_source_ids.size() != source_ids_.size() ||
|
||||
f.solid_results.size() != point_inventory_.size()) {
|
||||
return Failure(
|
||||
"Incomplete or out-of-order Solid endpoint inventory/schedule.");
|
||||
}
|
||||
for (std::size_t n = 0; n < source_ids_.size(); ++n)
|
||||
if (f.nodal_internal_order[n] != n ||
|
||||
!SameId(f.nodal_source_ids[n], source_ids_[n]))
|
||||
return Failure("Solid nodal source identity differs from its inventory.");
|
||||
const std::array<const Vector*, 10> high{
|
||||
&f.displacement, &f.velocity,
|
||||
&f.acceleration, &f.algorithmic_acceleration,
|
||||
&f.inertia_force, &f.internal_force,
|
||||
&f.external_force, &f.residual,
|
||||
&f.reaction, &f.residual_scale};
|
||||
const std::array<const Vector*, 10> low{
|
||||
&f.low.displacement, &f.low.velocity,
|
||||
&f.low.acceleration, &f.low.algorithmic_acceleration,
|
||||
&f.low.inertia_force, &f.low.internal_force,
|
||||
&f.low.external_force, &f.low.residual,
|
||||
&f.low.reaction, &f.low.residual_scale};
|
||||
for (std::size_t i = 0; i < high.size(); ++i)
|
||||
if (!ValidPair(*high[i], *low[i], full_dof_count_))
|
||||
return Failure(
|
||||
"Solid endpoint pairs are incomplete, unnormalized or "
|
||||
"nonfinite.");
|
||||
using namespace math_internal;
|
||||
for (std::size_t i = 0; i < full_dof_count_; ++i) {
|
||||
const bool constrained = std::binary_search(constrained_dofs_.begin(),
|
||||
constrained_dofs_.end(), i);
|
||||
const HighLow inertia(f.inertia_force[i], f.low.inertia_force[i]);
|
||||
const HighLow internal(f.internal_force[i], f.low.internal_force[i]);
|
||||
const HighLow external(f.external_force[i], f.low.external_force[i]);
|
||||
const HighLow residual(f.residual[i], f.low.residual[i]);
|
||||
const HighLow scale(f.residual_scale[i], f.low.residual_scale[i]);
|
||||
const HighLow reaction(f.reaction[i], f.low.reaction[i]);
|
||||
const auto expected_residual = Subtract(Add(inertia, internal), external);
|
||||
const auto expected_scale =
|
||||
Add(Add(Abs(inertia), Abs(internal)), Abs(external));
|
||||
if (!IsFinite(expected_residual) || !IsFinite(expected_scale) ||
|
||||
Compare(residual, expected_residual) != 0 ||
|
||||
Compare(scale, expected_scale) != 0 ||
|
||||
Compare(reaction, constrained ? residual : HighLow(0)) != 0)
|
||||
return Failure(
|
||||
"Physical reaction/residual/uncancelled force scale disagree.");
|
||||
if (constrained && (f.displacement[i] != 0 || f.low.displacement[i] != 0 ||
|
||||
f.velocity[i] != 0 || f.low.velocity[i] != 0 ||
|
||||
f.acceleration[i] != 0 || f.low.acceleration[i] != 0 ||
|
||||
f.algorithmic_acceleration[i] != 0 ||
|
||||
f.low.algorithmic_acceleration[i] != 0))
|
||||
return Failure(
|
||||
"Prescribed dynamic translations and rates must be exact zero.");
|
||||
if (!constrained) {
|
||||
if (Compare(scale, HighLow(0)) < 0)
|
||||
return Failure("Negative physical force scale.");
|
||||
if (Compare(scale, HighLow(0)) == 0) {
|
||||
if (Compare(residual, HighLow(0)) != 0)
|
||||
return Failure("Physical zero-scale residual must be exactly zero.");
|
||||
} else {
|
||||
const auto ratio = Divide(Abs(residual), scale);
|
||||
if (!IsFinite(ratio) || !IsNormalized(ratio) ||
|
||||
Compare(ratio, HighLow(1e-10)) > 0)
|
||||
return Failure("Physical free endpoint equilibrium is incomplete.");
|
||||
}
|
||||
}
|
||||
if (index == 0 &&
|
||||
(f.displacement[i] != 0 || f.velocity[i] != 0 ||
|
||||
f.low.displacement[i] != 0 || f.low.velocity[i] != 0 ||
|
||||
Compare(HighLow(f.acceleration[i], f.low.acceleration[i]),
|
||||
HighLow(f.algorithmic_acceleration[i],
|
||||
f.low.algorithmic_acceleration[i])) != 0))
|
||||
return Failure(
|
||||
"Initial Solid displacement/velocity must be zero and A=a.");
|
||||
}
|
||||
for (std::size_t i = 0; i < point_inventory_.size(); ++i) {
|
||||
const auto& row = f.solid_results[i];
|
||||
const auto& expected = point_inventory_[i];
|
||||
if (row.element != expected.element ||
|
||||
row.integration_point != expected.integration_point ||
|
||||
row.natural_coordinates != expected.natural_coordinates ||
|
||||
row.reference_coordinates != expected.reference_coordinates ||
|
||||
!Finite(row.cauchy_stress) || !Finite(row.strain))
|
||||
return Failure(
|
||||
"Solid point recovery has invalid identity or finite values.");
|
||||
}
|
||||
if (!ValidPair(f.strain_energy, f.low.strain_energy) ||
|
||||
!ValidPair(f.kinetic_energy, f.low.kinetic_energy) ||
|
||||
!ValidPair(f.external_work, f.low.external_work) ||
|
||||
!ValidPair(f.energy_balance_error, f.low.energy_balance_error))
|
||||
return Failure("Nonfinite or unnormalized Solid energy/work pair.");
|
||||
if ((index == 0 &&
|
||||
(f.external_work != 0 || f.low.external_work != 0 ||
|
||||
f.energy_balance_error != 0 || f.low.energy_balance_error != 0)) ||
|
||||
Compare(HighLow(f.kinetic_energy, f.low.kinetic_energy), HighLow(0)) < 0)
|
||||
return Failure(
|
||||
"Initial work/error or kinetic energy violates its definition.");
|
||||
if (!ValidPair(f.linear_momentum, f.low.linear_momentum) ||
|
||||
!ValidPair(f.angular_momentum, f.low.angular_momentum) ||
|
||||
!ValidPair(f.linear_momentum_rate, f.low.linear_momentum_rate) ||
|
||||
!ValidPair(f.angular_momentum_rate, f.low.angular_momentum_rate) ||
|
||||
!ValidPair(f.linearization_current_internal_moment,
|
||||
f.low.linearization_current_internal_moment) ||
|
||||
!ValidPair(f.reference_internal_moment,
|
||||
f.low.reference_internal_moment) ||
|
||||
!ValidPair(f.current_free_residual_moment,
|
||||
f.low.current_free_residual_moment))
|
||||
return Failure("Nonfinite or unnormalized physical momentum/moment pair.");
|
||||
if (!ValidPair(f.global_equilibrium, f.low.global_equilibrium) ||
|
||||
f.iterations.empty())
|
||||
return Failure("Missing finite equilibrium or iteration evidence.");
|
||||
for (std::size_t i = 0; i < f.iterations.size(); ++i) {
|
||||
const auto& e = f.iterations[i];
|
||||
if (e.iteration_index != i || !Finite(e.correction_metrics) ||
|
||||
!Finite(e.worst_force_terms) || !Finite(e.worst_weighted_terms))
|
||||
return Failure("Incomplete or nonfinite Newton correction evidence.");
|
||||
for (double x : {e.residual_numerator, e.residual_scale, e.residual_metric,
|
||||
e.weighted_residual_numerator, e.update_metric,
|
||||
e.old_residual_numerator, e.recombined_residual_numerator,
|
||||
e.weighted_equivalence_metric})
|
||||
if (!std::isfinite(x) || x < 0)
|
||||
return Failure("Invalid raw Newton residual/update evidence.");
|
||||
for (double x : e.correction_metrics)
|
||||
if (x < 0) return Failure("Invalid correction scale.");
|
||||
}
|
||||
const auto& last = f.iterations.back();
|
||||
if (f.iterations.size() != static_cast<std::size_t>(f.iteration_count) + 1 ||
|
||||
!last.residual_pass || !last.correction_pass || !last.update_pass ||
|
||||
!last.zero_scale_exact || last.residual_metric > 1e-10 ||
|
||||
last.update_metric > 1e-12 || last.weighted_equivalence_metric > 1e-12 ||
|
||||
std::any_of(last.correction_metrics.begin(),
|
||||
last.correction_metrics.end(),
|
||||
[](double x) { return x > 1e-10; }))
|
||||
return Failure(
|
||||
"Candidate does not carry complete accepted convergence evidence.");
|
||||
accepted_frames_.push_back(f);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Result<SolidDynamicAcceptedFrameState>
|
||||
SolidDynamicAnalysisState::BeginTrialFromCommitted() const {
|
||||
if (accepted_frames_.empty())
|
||||
return Result<SolidDynamicAcceptedFrameState>::Failure(
|
||||
Failure("No accepted Solid endpoint exists."));
|
||||
return Result<SolidDynamicAcceptedFrameState>::Success(
|
||||
accepted_frames_.back());
|
||||
}
|
||||
const std::vector<SolidDynamicAcceptedFrameState>&
|
||||
SolidDynamicAnalysisState::AcceptedFrames() const noexcept {
|
||||
return accepted_frames_;
|
||||
}
|
||||
const SolidDynamicAcceptedFrameState&
|
||||
SolidDynamicAnalysisState::CommittedState() const {
|
||||
if (accepted_frames_.empty())
|
||||
throw std::logic_error("No accepted Solid endpoint.");
|
||||
return accepted_frames_.back();
|
||||
}
|
||||
bool SolidDynamicAnalysisState::IsInitialized() const noexcept {
|
||||
return initialized_;
|
||||
}
|
||||
void SolidDynamicAnalysisState::RecordIteration(
|
||||
SolidDynamicIterationRecord record) {
|
||||
iteration_records_.push_back(std::move(record));
|
||||
}
|
||||
const std::vector<SolidDynamicIterationRecord>&
|
||||
SolidDynamicAnalysisState::IterationRecords() const noexcept {
|
||||
return iteration_records_;
|
||||
}
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,870 @@
|
||||
#include "fesa/analysis/solid_hht_dynamic_procedure.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/hht_time_integration.h"
|
||||
#include "fesa/assembly/load_assembler.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/constraints/essential_constraint_policy.h"
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/hex8_element.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
#include "fesa/solvers/linear/linear_solver.h"
|
||||
#include "fesa/solvers/linear/tangent_solver.h"
|
||||
#include "solid_hht_force_identity.h"
|
||||
#include "solid_hht_high_low.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
using math_internal::Abs;
|
||||
using math_internal::Add;
|
||||
using math_internal::Compare;
|
||||
using math_internal::Divide;
|
||||
using math_internal::HighLow;
|
||||
using math_internal::IsFinite;
|
||||
using math_internal::IsNormalized;
|
||||
using math_internal::Multiply;
|
||||
using math_internal::Negate;
|
||||
using math_internal::Rounded;
|
||||
using math_internal::Subtract;
|
||||
|
||||
HighLow Pair(const Vector& high, const Vector& low, std::size_t i) {
|
||||
return {high[i], low[i]};
|
||||
}
|
||||
void Store(HighLow value, double* high, double* low) {
|
||||
*high = value.high;
|
||||
*low = value.low;
|
||||
}
|
||||
void Store(HighLow value, Vector* high, Vector* low, std::size_t i) {
|
||||
Store(value, &(*high)[i], &(*low)[i]);
|
||||
}
|
||||
bool Valid(HighLow value) { return IsFinite(value) && IsNormalized(value); }
|
||||
HighLow Maximum(HighLow left, HighLow right) {
|
||||
if (!Valid(left) || !Valid(right)) return math_internal::InvalidHighLow();
|
||||
return Compare(left, right) >= 0 ? left : right;
|
||||
}
|
||||
void InitializeLowVectors(SolidDynamicAcceptedFrameState* f, std::size_t size) {
|
||||
for (auto* v : {&f->low.displacement, &f->low.velocity, &f->low.acceleration,
|
||||
&f->low.algorithmic_acceleration, &f->low.inertia_force,
|
||||
&f->low.internal_force, &f->low.external_force,
|
||||
&f->low.residual, &f->low.reaction, &f->low.residual_scale})
|
||||
*v = Vector(size);
|
||||
}
|
||||
struct PairedTangent {
|
||||
std::vector<HighLow> values;
|
||||
};
|
||||
/// @brief Reports source-context procedure failure without committing a trial.
|
||||
Status Failure(const Domain& domain, const char* code,
|
||||
const std::string& message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kSolver,
|
||||
{{Severity::kError, code,
|
||||
domain.Dynamic() == nullptr ? SourceLocation{}
|
||||
: domain.Dynamic()->dynamic_location,
|
||||
"DYNAMIC",
|
||||
domain.Dynamic() == nullptr ? ""
|
||||
: domain.Dynamic()->source_step_name_raw,
|
||||
message}});
|
||||
}
|
||||
Status CheckArithmeticEnvironment(const Domain& domain) {
|
||||
return math_internal::ArithmeticEnvironmentSupported()
|
||||
? Status::Ok()
|
||||
: Failure(domain, "solid-dynamic-arithmetic-environment",
|
||||
"Paired HHT requires nearest rounding and gradual "
|
||||
"underflow, including after backend callbacks.");
|
||||
}
|
||||
/// @brief Owns immutable kernels and the stable full-space translation scatter.
|
||||
struct RuntimeSolid {
|
||||
std::unique_ptr<Element> owner;
|
||||
const Hex8Element* solid;
|
||||
std::vector<std::size_t> scatter;
|
||||
};
|
||||
/// @brief Creates checked kernels in declaration order, with no mutable element
|
||||
/// state.
|
||||
Result<std::vector<RuntimeSolid>> BuildSolids(const Domain& domain,
|
||||
const DofManager& dofs) {
|
||||
std::vector<RuntimeSolid> solids;
|
||||
ElementFactory factory;
|
||||
for (std::size_t e = 0; e < domain.Elements().Size(); ++e) {
|
||||
auto owner = factory.Create(domain.Elements()[e], domain);
|
||||
if (!owner.HasValue())
|
||||
return Result<std::vector<RuntimeSolid>>::Failure(owner.GetStatus());
|
||||
const auto* solid = dynamic_cast<const Hex8Element*>(owner.Value().get());
|
||||
if (solid == nullptr)
|
||||
return Result<std::vector<RuntimeSolid>>::Failure(
|
||||
Failure(domain, "solid-dynamic-element-family",
|
||||
"HHT requires exclusively Hex8 kernels."));
|
||||
auto scatter = dofs.ElementScatter(solid->DofLayout());
|
||||
if (!scatter.HasValue())
|
||||
return Result<std::vector<RuntimeSolid>>::Failure(scatter.GetStatus());
|
||||
solids.push_back(
|
||||
{std::move(owner.Value()), solid, std::move(scatter.Value())});
|
||||
}
|
||||
return Result<std::vector<RuntimeSolid>>::Success(std::move(solids));
|
||||
}
|
||||
/// @brief Preserves all 576 structural entries in stable element/local order.
|
||||
void AppendMatrix(const Matrix& local, const RuntimeSolid& solid, std::size_t e,
|
||||
std::vector<CooContribution>* coo) {
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
for (std::size_t j = 0; j < 24; ++j)
|
||||
coo->push_back(
|
||||
{solid.scatter[i], solid.scatter[j], local(i, j), e, 24 * i + j});
|
||||
}
|
||||
/// @brief Assembles constant reference consistent mass once without lumping.
|
||||
Result<SparseMatrix> AssembleMass(const std::vector<RuntimeSolid>& solids,
|
||||
const DofManager& dofs) {
|
||||
std::vector<CooContribution> coo;
|
||||
coo.reserve(solids.size() * 576);
|
||||
for (std::size_t e = 0; e < solids.size(); ++e) {
|
||||
auto mass = solids[e].solid->ComputeMass();
|
||||
if (!mass.HasValue())
|
||||
return Result<SparseMatrix>::Failure(mass.GetStatus());
|
||||
AppendMatrix(mass.Value().values, solids[e], e, &coo);
|
||||
}
|
||||
return SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(),
|
||||
std::move(coo), dofs.GetSparsePattern());
|
||||
}
|
||||
/// @brief Evaluates independent local trials, then deterministically reduces
|
||||
/// recovery.
|
||||
Result<PairedTangent> EvaluateSolids(const Domain& domain,
|
||||
const std::vector<RuntimeSolid>& solids,
|
||||
const DofManager& dofs,
|
||||
const ParallelFor& parallel,
|
||||
SolidDynamicAcceptedFrameState* f) {
|
||||
const auto environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return Result<PairedTangent>::Failure(environment);
|
||||
if (!detail::ValidSolidHhtPair(f->displacement, f->low.displacement,
|
||||
dofs.FullDofCount()))
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-invalid-paired-state",
|
||||
"Local evaluation requires complete normalized pairs."));
|
||||
std::vector<std::optional<Result<Hex8HighLowResponse>>> responses(
|
||||
solids.size());
|
||||
parallel.Execute(solids.size(), [&](std::size_t e) {
|
||||
Vector local(24), low(24);
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
local[i] = f->displacement[solids[e].scatter[i]];
|
||||
low[i] = f->low.displacement[solids[e].scatter[i]];
|
||||
}
|
||||
responses[e] = domain.Dynamic()->nlgeom
|
||||
? solids[e].solid->EvaluateNonlinearHighLow(local, low)
|
||||
: solids[e].solid->EvaluateLinearHighLow(local, low);
|
||||
});
|
||||
const auto reduction_environment = CheckArithmeticEnvironment(domain);
|
||||
if (!reduction_environment.IsOk())
|
||||
return Result<PairedTangent>::Failure(reduction_environment);
|
||||
const auto& pattern = dofs.GetSparsePattern();
|
||||
PairedTangent tangent;
|
||||
tangent.values.resize(pattern.column_indices.size());
|
||||
f->internal_force = Vector(dofs.FullDofCount());
|
||||
f->low.internal_force = Vector(dofs.FullDofCount());
|
||||
f->strain_energy = 0;
|
||||
f->low.strain_energy = 0;
|
||||
f->solid_results.clear();
|
||||
for (std::size_t e = 0; e < solids.size(); ++e) {
|
||||
if (!responses[e])
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-local-execution",
|
||||
"Local worker did not evaluate its index."));
|
||||
if (!responses[e]->HasValue())
|
||||
return Result<PairedTangent>::Failure(responses[e]->GetStatus());
|
||||
const auto& r = responses[e]->Value();
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
const auto row = solids[e].scatter[i];
|
||||
const auto force =
|
||||
Add(Pair(f->internal_force, f->low.internal_force, row),
|
||||
Pair(r.high.internal_force, r.internal_force_low, i));
|
||||
if (!Valid(force))
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-nonfinite-assembly",
|
||||
"Paired internal force reduction failed."));
|
||||
Store(force, &f->internal_force, &f->low.internal_force, row);
|
||||
for (std::size_t j = 0; j < 24; ++j) {
|
||||
const auto column = solids[e].scatter[j];
|
||||
const auto begin =
|
||||
pattern.column_indices.begin() + pattern.row_offsets[row];
|
||||
const auto end =
|
||||
pattern.column_indices.begin() + pattern.row_offsets[row + 1];
|
||||
const auto found = std::lower_bound(begin, end, column);
|
||||
if (found == end || *found != column)
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-sparse-pattern",
|
||||
"Local tangent is outside the fixed sparse pattern."));
|
||||
const auto k =
|
||||
static_cast<std::size_t>(found - pattern.column_indices.begin());
|
||||
tangent.values[k] =
|
||||
Add(tangent.values[k],
|
||||
HighLow(r.high.tangent(i, j), r.tangent_low(i, j)));
|
||||
if (!Valid(tangent.values[k]))
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-nonfinite-assembly",
|
||||
"Paired tangent reduction failed."));
|
||||
}
|
||||
}
|
||||
const auto energy = Add(HighLow(f->strain_energy, f->low.strain_energy),
|
||||
HighLow(r.high.strain_energy, r.strain_energy_low));
|
||||
if (!Valid(energy))
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-nonfinite-assembly",
|
||||
"Paired strain energy reduction failed."));
|
||||
Store(energy, &f->strain_energy, &f->low.strain_energy);
|
||||
f->solid_results.insert(f->solid_results.end(), r.high.points.begin(),
|
||||
r.high.points.end());
|
||||
}
|
||||
return Result<PairedTangent>::Success(std::move(tangent));
|
||||
}
|
||||
/// @brief Returns the finite positive reference nodal bounding-box diagonal.
|
||||
Result<double> ReferenceLength(const Domain& domain) {
|
||||
auto lo = domain.Nodes().front().coordinates;
|
||||
auto hi = lo;
|
||||
for (const auto& node : domain.Nodes())
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
lo[c] = (std::min)(lo[c], node.coordinates[c]);
|
||||
hi[c] = (std::max)(hi[c], node.coordinates[c]);
|
||||
}
|
||||
const double length =
|
||||
std::hypot(std::hypot(hi[0] - lo[0], hi[1] - lo[1]), hi[2] - lo[2]);
|
||||
if (!std::isfinite(length) || length <= 0)
|
||||
return Result<double>::Failure(
|
||||
Failure(domain, "solid-dynamic-reference-length",
|
||||
"Reference length is not finite positive."));
|
||||
return Result<double>::Success(length);
|
||||
}
|
||||
/// @brief Forms M/(beta*h²)+(1+alpha)*Kt in the unchanged full sparse pattern.
|
||||
Result<SparseMatrix> EffectiveTangent(const SparseMatrix& m,
|
||||
const PairedTangent& k,
|
||||
const DofManager& dofs, double h,
|
||||
const HhtParameters& p) {
|
||||
std::vector<CooContribution> coo;
|
||||
coo.reserve(m.Values().size());
|
||||
const auto denominator =
|
||||
Multiply(Multiply(HighLow(p.beta), HighLow(h)), HighLow(h));
|
||||
const auto coefficient = Add(HighLow(1), HighLow(p.alpha));
|
||||
for (std::size_t i = 0; i < m.Rows(); ++i)
|
||||
for (std::size_t j = m.RowOffsets()[i]; j < m.RowOffsets()[i + 1]; ++j) {
|
||||
const auto value = Add(Divide(HighLow(m.Values()[j]), denominator),
|
||||
Multiply(coefficient, k.values[j]));
|
||||
if (!Valid(value))
|
||||
return Result<SparseMatrix>::Failure(
|
||||
detail::SolidHhtPairedKinematicsFailure());
|
||||
coo.push_back({i, m.ColumnIndices()[j], Rounded(value), 0, j});
|
||||
}
|
||||
return SparseMatrix::FromCoo(m.Rows(), m.Columns(), std::move(coo),
|
||||
dofs.GetSparsePattern());
|
||||
}
|
||||
|
||||
using Paired3 = std::array<HighLow, 3>;
|
||||
void CrossAdd(const Paired3& x, const Paired3& force, Paired3* sum) {
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
const auto j = (c + 1) % 3;
|
||||
const auto k = (c + 2) % 3;
|
||||
(*sum)[c] = Add((*sum)[c], Subtract(Multiply(x[j], force[k]),
|
||||
Multiply(x[k], force[j])));
|
||||
}
|
||||
}
|
||||
bool Store3(const Paired3& values, std::array<double, 3>* high,
|
||||
std::array<double, 3>* low) {
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
if (!Valid(values[c])) return false;
|
||||
Store(values[c], &(*high)[c], &(*low)[c]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @brief Recovers full physical endpoint quantities before any publication or
|
||||
/// commit. The constant Float64 mass coefficients are lifted exactly once.
|
||||
Status RecoverEndpoint(const Domain& domain, const DofManager& dofs,
|
||||
const SparseMatrix& mass,
|
||||
const SolidDynamicAcceptedFrameState* old,
|
||||
HighLow initial_energy,
|
||||
SolidDynamicAcceptedFrameState* f,
|
||||
SolidDynamicIterationEvidence* e) {
|
||||
auto inertia = detail::MultiplySolidHhtMassHighLow(mass, f->acceleration,
|
||||
f->low.acceleration);
|
||||
auto momentum =
|
||||
detail::MultiplySolidHhtMassHighLow(mass, f->velocity, f->low.velocity);
|
||||
if (!inertia.HasValue()) return inertia.GetStatus();
|
||||
if (!momentum.HasValue()) return momentum.GetStatus();
|
||||
f->inertia_force = std::move(inertia.Value().high);
|
||||
f->low.inertia_force = std::move(inertia.Value().low);
|
||||
const auto size = dofs.FullDofCount();
|
||||
for (auto* v : {&f->residual, &f->low.residual, &f->reaction,
|
||||
&f->low.reaction, &f->residual_scale, &f->low.residual_scale})
|
||||
*v = Vector(size);
|
||||
HighLow numerator, scale_maximum, metric;
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
const auto ma = Pair(f->inertia_force, f->low.inertia_force, i);
|
||||
const auto internal = Pair(f->internal_force, f->low.internal_force, i);
|
||||
const auto external = Pair(f->external_force, f->low.external_force, i);
|
||||
const auto residual = Subtract(Add(ma, internal), external);
|
||||
const auto scale = Add(Add(Abs(ma), Abs(internal)), Abs(external));
|
||||
if (!Valid(residual) || !Valid(scale))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-residual",
|
||||
"Physical paired force arithmetic failed.");
|
||||
Store(residual, &f->residual, &f->low.residual, i);
|
||||
Store(scale, &f->residual_scale, &f->low.residual_scale, i);
|
||||
}
|
||||
for (auto i : dofs.ConstrainedDofs()) {
|
||||
f->reaction[i] = f->residual[i];
|
||||
f->low.reaction[i] = f->low.residual[i];
|
||||
}
|
||||
for (auto i : dofs.FreeDofs()) {
|
||||
const auto residual = Pair(f->residual, f->low.residual, i);
|
||||
const auto scale = Pair(f->residual_scale, f->low.residual_scale, i);
|
||||
numerator = Maximum(numerator, Abs(residual));
|
||||
scale_maximum = Maximum(scale_maximum, scale);
|
||||
if (Compare(scale, HighLow(0)) == 0) {
|
||||
e->zero_scale_exact =
|
||||
e->zero_scale_exact && Compare(residual, HighLow(0)) == 0;
|
||||
} else {
|
||||
const auto ratio = Divide(Abs(residual), scale);
|
||||
if (!Valid(ratio))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-residual",
|
||||
"Physical paired force ratio is unsupported.");
|
||||
if (Compare(ratio, metric) > 0) {
|
||||
metric = ratio;
|
||||
e->worst_full_dof = i;
|
||||
e->worst_force_terms = {
|
||||
Rounded(residual),
|
||||
Rounded(Pair(f->inertia_force, f->low.inertia_force, i)),
|
||||
Rounded(Pair(f->internal_force, f->low.internal_force, i)),
|
||||
Rounded(Pair(f->external_force, f->low.external_force, i)),
|
||||
Rounded(scale)};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Valid(numerator) || !Valid(scale_maximum) || !Valid(metric))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-residual",
|
||||
"Physical paired evidence is nonfinite.");
|
||||
e->residual_numerator = Rounded(numerator);
|
||||
e->residual_scale = Rounded(scale_maximum);
|
||||
e->residual_metric = Rounded(metric);
|
||||
e->residual_pass =
|
||||
e->zero_scale_exact && Compare(metric, HighLow(1e-10)) <= 0;
|
||||
|
||||
HighLow kinetic;
|
||||
HighLow work = old == nullptr
|
||||
? HighLow(0)
|
||||
: HighLow(old->external_work, old->low.external_work);
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
kinetic =
|
||||
Add(kinetic,
|
||||
Multiply(Pair(f->velocity, f->low.velocity, i),
|
||||
Pair(momentum.Value().high, momentum.Value().low, i)));
|
||||
if (old != nullptr) {
|
||||
const auto average =
|
||||
Multiply(HighLow(.5),
|
||||
Add(Pair(old->external_force, old->low.external_force, i),
|
||||
Pair(f->external_force, f->low.external_force, i)));
|
||||
const auto delta =
|
||||
Subtract(Pair(f->displacement, f->low.displacement, i),
|
||||
Pair(old->displacement, old->low.displacement, i));
|
||||
work = Add(work, Multiply(average, delta));
|
||||
}
|
||||
}
|
||||
kinetic = Multiply(HighLow(.5), kinetic);
|
||||
const auto error = Subtract(
|
||||
Subtract(Add(kinetic, HighLow(f->strain_energy, f->low.strain_energy)),
|
||||
initial_energy),
|
||||
work);
|
||||
if (!Valid(kinetic) || !Valid(work) || !Valid(error))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-energy",
|
||||
"Paired energy or endpoint work is unsupported.");
|
||||
Store(kinetic, &f->kinetic_energy, &f->low.kinetic_energy);
|
||||
Store(work, &f->external_work, &f->low.external_work);
|
||||
Store(error, &f->energy_balance_error, &f->low.energy_balance_error);
|
||||
|
||||
Paired3 linear{}, angular{}, linear_rate{}, angular_rate{}, applied_moment{},
|
||||
linear_equilibrium{}, current_internal_moment{}, reference_moment{},
|
||||
free_residual_moment{};
|
||||
for (std::size_t n = 0; n < domain.Nodes().size(); ++n) {
|
||||
Paired3 x{}, reference{}, mv{}, ma{}, applied{}, internal{},
|
||||
free_residual{}, u{};
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
const auto i = 3 * n + c;
|
||||
reference[c] = HighLow(domain.Nodes()[n].coordinates[c]);
|
||||
u[c] = Pair(f->displacement, f->low.displacement, i);
|
||||
x[c] = Add(reference[c], u[c]);
|
||||
mv[c] = Pair(momentum.Value().high, momentum.Value().low, i);
|
||||
ma[c] = Pair(f->inertia_force, f->low.inertia_force, i);
|
||||
internal[c] = Pair(f->internal_force, f->low.internal_force, i);
|
||||
applied[c] = Add(Pair(f->external_force, f->low.external_force, i),
|
||||
Pair(f->reaction, f->low.reaction, i));
|
||||
free_residual[c] = Subtract(Pair(f->residual, f->low.residual, i),
|
||||
Pair(f->reaction, f->low.reaction, i));
|
||||
linear[c] = Add(linear[c], mv[c]);
|
||||
linear_rate[c] = Add(linear_rate[c], ma[c]);
|
||||
linear_equilibrium[c] =
|
||||
Add(linear_equilibrium[c], Subtract(applied[c], ma[c]));
|
||||
}
|
||||
CrossAdd(x, mv, &angular);
|
||||
CrossAdd(x, ma, &angular_rate);
|
||||
CrossAdd(x, applied, &applied_moment);
|
||||
CrossAdd(u, internal, ¤t_internal_moment);
|
||||
CrossAdd(reference, internal, &reference_moment);
|
||||
CrossAdd(x, free_residual, &free_residual_moment);
|
||||
}
|
||||
if (!Store3(linear, &f->linear_momentum, &f->low.linear_momentum) ||
|
||||
!Store3(angular, &f->angular_momentum, &f->low.angular_momentum) ||
|
||||
!Store3(linear_rate, &f->linear_momentum_rate,
|
||||
&f->low.linear_momentum_rate) ||
|
||||
!Store3(angular_rate, &f->angular_momentum_rate,
|
||||
&f->low.angular_momentum_rate) ||
|
||||
!Store3(current_internal_moment,
|
||||
&f->linearization_current_internal_moment,
|
||||
&f->low.linearization_current_internal_moment) ||
|
||||
!Store3(reference_moment, &f->reference_internal_moment,
|
||||
&f->low.reference_internal_moment) ||
|
||||
!Store3(free_residual_moment, &f->current_free_residual_moment,
|
||||
&f->low.current_free_residual_moment))
|
||||
return Failure(
|
||||
domain, "solid-dynamic-nonfinite-momentum",
|
||||
"Paired momentum or current/reference moment is unsupported.");
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
const auto moment = Subtract(applied_moment[c], angular_rate[c]);
|
||||
if (!Valid(linear_equilibrium[c]) || !Valid(moment))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-equilibrium",
|
||||
"Paired global equilibrium is unsupported.");
|
||||
Store(linear_equilibrium[c], &f->global_equilibrium[c],
|
||||
&f->low.global_equilibrium[c]);
|
||||
Store(moment, &f->global_equilibrium[3 + c],
|
||||
&f->low.global_equilibrium[3 + c]);
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
/// @brief Uses full paired same-unit uncancelled formula terms.
|
||||
HighLow UpdateMetric(HighLow left, std::initializer_list<HighLow> terms) {
|
||||
HighLow sum;
|
||||
auto scale = Abs(left);
|
||||
for (const auto x : terms) {
|
||||
sum = Add(sum, x);
|
||||
scale = Add(scale, Abs(x));
|
||||
}
|
||||
const auto defect = Abs(Subtract(left, sum));
|
||||
if (!Valid(defect) || !Valid(scale)) return math_internal::InvalidHighLow();
|
||||
if (Compare(scale, HighLow(0)) == 0)
|
||||
return Compare(defect, HighLow(0)) == 0 ? HighLow(0)
|
||||
: math_internal::InvalidHighLow();
|
||||
return Divide(defect, scale);
|
||||
}
|
||||
HighLow CheckUpdates(const SolidDynamicAcceptedFrameState& old,
|
||||
const SolidDynamicAcceptedFrameState& f,
|
||||
const HhtParameters& p, double h) {
|
||||
HighLow metric;
|
||||
const auto step = HighLow(h);
|
||||
const auto hh = Multiply(step, step);
|
||||
for (std::size_t i = 0; i < f.displacement.Size(); ++i) {
|
||||
const auto A =
|
||||
Pair(f.algorithmic_acceleration, f.low.algorithmic_acceleration, i);
|
||||
const auto oldA =
|
||||
Pair(old.algorithmic_acceleration, old.low.algorithmic_acceleration, i);
|
||||
const auto oldv = Pair(old.velocity, old.low.velocity, i);
|
||||
metric = Maximum(
|
||||
metric,
|
||||
UpdateMetric(
|
||||
Pair(f.displacement, f.low.displacement, i),
|
||||
{Pair(old.displacement, old.low.displacement, i),
|
||||
Multiply(step, oldv),
|
||||
Multiply(Multiply(hh, Subtract(HighLow(.5), HighLow(p.beta))),
|
||||
oldA),
|
||||
Multiply(Multiply(hh, HighLow(p.beta)), A)}));
|
||||
metric = Maximum(
|
||||
metric,
|
||||
UpdateMetric(
|
||||
Pair(f.velocity, f.low.velocity, i),
|
||||
{oldv,
|
||||
Multiply(Multiply(step, Subtract(HighLow(1), HighLow(p.gamma))),
|
||||
oldA),
|
||||
Multiply(Multiply(step, HighLow(p.gamma)), A)}));
|
||||
metric = Maximum(
|
||||
metric,
|
||||
UpdateMetric(A, {Multiply(Add(HighLow(1), HighLow(p.alpha)),
|
||||
Pair(f.acceleration, f.low.acceleration, i)),
|
||||
Negate(Multiply(HighLow(p.alpha),
|
||||
Pair(old.acceleration,
|
||||
old.low.acceleration, i)))}));
|
||||
}
|
||||
return metric;
|
||||
}
|
||||
std::array<HighLow, 4> FreshCorrectionMetrics(const Vector& du, double length,
|
||||
const HhtParameters& p) {
|
||||
HighLow q;
|
||||
for (std::size_t i = 0; i < du.Size(); ++i)
|
||||
q = Maximum(q, Divide(Abs(HighLow(du[i])), HighLow(length)));
|
||||
return {
|
||||
q, Divide(Multiply(HighLow(p.gamma), q), HighLow(p.beta)),
|
||||
Divide(q, Multiply(Add(HighLow(1), HighLow(p.alpha)), HighLow(p.beta))),
|
||||
Divide(q, HighLow(p.beta))};
|
||||
}
|
||||
std::array<HighLow, 4> ObservedCorrectionMetrics(
|
||||
const SolidDynamicAcceptedFrameState& before,
|
||||
const SolidDynamicAcceptedFrameState& after, double h, double length) {
|
||||
std::array<HighLow, 4> metric{};
|
||||
const auto hh = Multiply(HighLow(h), HighLow(h));
|
||||
for (std::size_t i = 0; i < after.displacement.Size(); ++i) {
|
||||
metric[0] = Maximum(
|
||||
metric[0],
|
||||
Divide(Abs(Subtract(
|
||||
Pair(after.displacement, after.low.displacement, i),
|
||||
Pair(before.displacement, before.low.displacement, i))),
|
||||
HighLow(length)));
|
||||
metric[1] = Maximum(
|
||||
metric[1],
|
||||
Divide(Multiply(HighLow(h),
|
||||
Abs(Subtract(
|
||||
Pair(after.velocity, after.low.velocity, i),
|
||||
Pair(before.velocity, before.low.velocity, i)))),
|
||||
HighLow(length)));
|
||||
metric[2] = Maximum(
|
||||
metric[2],
|
||||
Divide(Multiply(hh, Abs(Subtract(Pair(after.acceleration,
|
||||
after.low.acceleration, i),
|
||||
Pair(before.acceleration,
|
||||
before.low.acceleration, i)))),
|
||||
HighLow(length)));
|
||||
metric[3] = Maximum(
|
||||
metric[3],
|
||||
Divide(Multiply(hh, Abs(Subtract(
|
||||
Pair(after.algorithmic_acceleration,
|
||||
after.low.algorithmic_acceleration, i),
|
||||
Pair(before.algorithmic_acceleration,
|
||||
before.low.algorithmic_acceleration, i)))),
|
||||
HighLow(length)));
|
||||
}
|
||||
return metric;
|
||||
}
|
||||
bool CorrectionPass(const std::array<HighLow, 4>& metrics) {
|
||||
return std::all_of(metrics.begin(), metrics.end(), [](HighLow x) {
|
||||
return Valid(x) && Compare(x, HighLow(0)) >= 0 &&
|
||||
Compare(x, HighLow(1e-10)) <= 0;
|
||||
});
|
||||
}
|
||||
Status SetRates(const SolidDynamicAcceptedFrameState& old,
|
||||
const detail::SolidHhtPairedPredictor& predictor, double h,
|
||||
const HhtParameters& p,
|
||||
SolidDynamicAcceptedFrameState* candidate) {
|
||||
auto rates = detail::EvaluateSolidHhtHighLowRates(
|
||||
candidate->displacement, candidate->low.displacement, predictor,
|
||||
old.acceleration, old.low.acceleration, h, p);
|
||||
if (!rates.HasValue()) return rates.GetStatus();
|
||||
candidate->velocity = std::move(rates.Value().velocity);
|
||||
candidate->low.velocity = std::move(rates.Value().velocity_low);
|
||||
candidate->acceleration = std::move(rates.Value().acceleration);
|
||||
candidate->low.acceleration = std::move(rates.Value().acceleration_low);
|
||||
candidate->algorithmic_acceleration =
|
||||
std::move(rates.Value().algorithmic_acceleration);
|
||||
candidate->low.algorithmic_acceleration =
|
||||
std::move(rates.Value().algorithmic_acceleration_low);
|
||||
return Status::Ok();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SolidHhtDynamicProcedureExecutor::SolidHhtDynamicProcedureExecutor(
|
||||
const ParallelFor& parallel_for, LinearSolver& mass_solver,
|
||||
TangentSolver& tangent_solver)
|
||||
: parallel_for_(parallel_for),
|
||||
mass_solver_(mass_solver),
|
||||
tangent_solver_(tangent_solver) {}
|
||||
|
||||
Result<SolidDynamicExecutionResult> SolidHhtDynamicProcedureExecutor::Execute(
|
||||
const Domain& domain) const {
|
||||
auto model = AnalysisModel::Create(domain);
|
||||
if (!model.HasValue())
|
||||
return Result<SolidDynamicExecutionResult>::Failure(model.GetStatus());
|
||||
auto dofs = DofManager::Create(model.Value());
|
||||
if (!dofs.HasValue())
|
||||
return Result<SolidDynamicExecutionResult>::Failure(dofs.GetStatus());
|
||||
auto state = SolidDynamicAnalysisState::Create(domain, dofs.Value());
|
||||
if (!state.HasValue())
|
||||
return Result<SolidDynamicExecutionResult>::Failure(state.GetStatus());
|
||||
auto status = ExecuteInto(domain, state.Value());
|
||||
if (!status.IsOk())
|
||||
return Result<SolidDynamicExecutionResult>::Failure(status);
|
||||
return Result<SolidDynamicExecutionResult>::Success(
|
||||
{std::move(dofs.Value()), std::move(state.Value())});
|
||||
}
|
||||
Status SolidHhtDynamicProcedureExecutor::ExecuteInto(
|
||||
const Domain& domain, SolidDynamicAnalysisState& state) const {
|
||||
return ExecuteIntoForVerification(domain, state, -0.05);
|
||||
}
|
||||
Status SolidHhtDynamicProcedureExecutor::ExecuteIntoForVerification(
|
||||
const Domain& domain, SolidDynamicAnalysisState& state,
|
||||
double alpha) const {
|
||||
SolidDynamicIterationRecord record;
|
||||
auto fail = [&](const Status& failure) {
|
||||
record.stage = "failed";
|
||||
record.outcome = "failed";
|
||||
record.failure_code = failure.Diagnostics().empty()
|
||||
? "solid-dynamic-failure"
|
||||
: failure.Diagnostics().front().code;
|
||||
state.RecordIteration(record);
|
||||
return failure;
|
||||
};
|
||||
if (!state.IsInitialized() || !state.AcceptedFrames().empty() ||
|
||||
!domain.IsSolid() || domain.Dynamic() == nullptr)
|
||||
return fail(Failure(
|
||||
domain, "solid-dynamic-invalid-execution",
|
||||
"Execution requires an initialized empty Solid dynamic state."));
|
||||
auto environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
auto model = AnalysisModel::Create(domain);
|
||||
if (!model.HasValue()) return fail(model.GetStatus());
|
||||
auto dofs_result = DofManager::Create(model.Value());
|
||||
if (!dofs_result.HasValue()) return fail(dofs_result.GetStatus());
|
||||
const auto& dofs = dofs_result.Value();
|
||||
auto solids = BuildSolids(domain, dofs);
|
||||
if (!solids.HasValue()) return fail(solids.GetStatus());
|
||||
auto mass = AssembleMass(solids.Value(), dofs);
|
||||
if (!mass.HasValue()) return fail(mass.GetStatus());
|
||||
EssentialConstraintPolicy partition;
|
||||
auto mass_partition = partition.Partition(mass.Value(), dofs);
|
||||
if (!mass_partition.HasValue()) return fail(mass_partition.GetStatus());
|
||||
if (dofs.FreeDofCount() != 0) {
|
||||
const auto status = mass_solver_.Factorize(mass_partition.Value().k_ff);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
}
|
||||
auto parameters = MakeHhtParameters(alpha);
|
||||
if (!parameters.HasValue()) return fail(parameters.GetStatus());
|
||||
const auto p = parameters.Value();
|
||||
auto length = ReferenceLength(domain);
|
||||
if (!length.HasValue()) return fail(length.GetStatus());
|
||||
const double h = domain.Dynamic()->initial_increment;
|
||||
SolidDynamicAcceptedFrameState initial;
|
||||
initial.is_initial = true;
|
||||
for (std::size_t n = 0; n < domain.Nodes().size(); ++n) {
|
||||
initial.nodal_internal_order.push_back(static_cast<EntityIndex>(n));
|
||||
initial.nodal_source_ids.push_back(domain.Nodes()[n].source_id);
|
||||
}
|
||||
for (auto* v : {&initial.displacement, &initial.velocity,
|
||||
&initial.acceleration, &initial.algorithmic_acceleration})
|
||||
*v = Vector(dofs.FullDofCount());
|
||||
InitializeLowVectors(&initial, dofs.FullDofCount());
|
||||
auto force = LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs, 0);
|
||||
if (!force.HasValue()) return fail(force.GetStatus());
|
||||
initial.external_force = std::move(force.Value());
|
||||
auto tangent =
|
||||
EvaluateSolids(domain, solids.Value(), dofs, parallel_for_, &initial);
|
||||
if (!tangent.HasValue()) return fail(tangent.GetStatus());
|
||||
SolidDynamicIterationEvidence evidence;
|
||||
bool initial_converged = false;
|
||||
for (std::uint32_t refinement = 0; refinement <= 100; ++refinement) {
|
||||
initial.algorithmic_acceleration = initial.acceleration;
|
||||
initial.low.algorithmic_acceleration = initial.low.acceleration;
|
||||
evidence = {};
|
||||
evidence.correction_pass = evidence.update_pass = true;
|
||||
auto status = RecoverEndpoint(domain, dofs, mass.Value(), nullptr,
|
||||
HighLow(0), &initial, &evidence);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
if (evidence.residual_pass) {
|
||||
initial_converged = true;
|
||||
break;
|
||||
}
|
||||
record.stage = "initial-mass";
|
||||
record.outcome = "residual-refinement";
|
||||
record.evidence = evidence;
|
||||
record.evidence.iteration_index = refinement;
|
||||
state.RecordIteration(record);
|
||||
if (refinement == 100) break;
|
||||
Vector rhs(dofs.FreeDofCount()), a(dofs.FreeDofCount());
|
||||
for (std::size_t j = 0; j < dofs.FreeDofCount(); ++j) {
|
||||
const auto i = dofs.FreeDofs()[j];
|
||||
rhs[j] = Rounded(Negate(Pair(initial.residual, initial.low.residual, i)));
|
||||
}
|
||||
status = mass_solver_.Solve(rhs, a);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
if (a.Size() != dofs.FreeDofCount())
|
||||
return fail(
|
||||
Failure(domain, "solid-dynamic-invalid-backend-result",
|
||||
"Initial mass solve returned incompatible acceleration."));
|
||||
for (std::size_t j = 0; j < a.Size(); ++j) {
|
||||
if (!std::isfinite(a[j]))
|
||||
return fail(
|
||||
Failure(domain, "solid-dynamic-invalid-backend-result",
|
||||
"Initial mass solve returned nonfinite acceleration."));
|
||||
const auto i = dofs.FreeDofs()[j];
|
||||
const auto value =
|
||||
Add(Pair(initial.acceleration, initial.low.acceleration, i),
|
||||
HighLow(a[j] == 0 ? 0.0 : a[j]));
|
||||
if (!Valid(value)) return fail(detail::SolidHhtPairedKinematicsFailure());
|
||||
Store(value, &initial.acceleration, &initial.low.acceleration, i);
|
||||
}
|
||||
}
|
||||
if (!initial_converged)
|
||||
return fail(Failure(domain, "solid-dynamic-initial-mass-nonconvergence",
|
||||
"Physical initial equilibrium did not converge in100 "
|
||||
"mass refinements."));
|
||||
initial.iterations.push_back(evidence);
|
||||
auto status = state.CommitAcceptedFrame(initial);
|
||||
if (!status.IsOk()) {
|
||||
record.evidence = evidence;
|
||||
return fail(status);
|
||||
}
|
||||
record.stage = "accepted";
|
||||
record.outcome = "initial";
|
||||
record.evidence = evidence;
|
||||
state.RecordIteration(record);
|
||||
const auto initial_energy =
|
||||
Add(HighLow(initial.strain_energy, initial.low.strain_energy),
|
||||
HighLow(initial.kinetic_energy, initial.low.kinetic_energy));
|
||||
for (std::uint32_t increment = 1;
|
||||
increment <= domain.Dynamic()->increment_count; ++increment) {
|
||||
const auto& old = state.CommittedState();
|
||||
record = {};
|
||||
record.increment_index = increment;
|
||||
record.time = static_cast<double>(increment) * h;
|
||||
auto predictor = detail::PredictSolidHhtHighLow(old, h, p);
|
||||
if (!predictor.HasValue()) return fail(predictor.GetStatus());
|
||||
auto candidate = old;
|
||||
candidate.frame_index = candidate.increment_index = increment;
|
||||
candidate.iteration_count = 0;
|
||||
candidate.time = record.time;
|
||||
candidate.is_initial = false;
|
||||
candidate.iterations.clear();
|
||||
// Begin from certified accepted geometry, not an unchecked predictor.
|
||||
force = LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs,
|
||||
candidate.time);
|
||||
if (!force.HasValue()) return fail(force.GetStatus());
|
||||
candidate.external_force = std::move(force.Value());
|
||||
candidate.low.external_force = Vector(dofs.FullDofCount());
|
||||
std::array<HighLow, 4> correction{}, fresh_correction{};
|
||||
bool accepted = false;
|
||||
for (std::uint32_t iteration = 0; iteration <= 100; ++iteration) {
|
||||
record.stage = "evaluation";
|
||||
record.outcome = "paired-candidate";
|
||||
record.evidence = {};
|
||||
record.evidence.iteration_index = iteration;
|
||||
status = SetRates(old, predictor.Value(), h, p, &candidate);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
tangent = EvaluateSolids(domain, solids.Value(), dofs, parallel_for_,
|
||||
&candidate);
|
||||
if (!tangent.HasValue()) return fail(tangent.GetStatus());
|
||||
status = RecoverEndpoint(domain, dofs, mass.Value(), &old, initial_energy,
|
||||
&candidate, &record.evidence);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
auto MA = detail::MultiplySolidHhtMassHighLow(
|
||||
mass.Value(), candidate.algorithmic_acceleration,
|
||||
candidate.low.algorithmic_acceleration);
|
||||
if (!MA.HasValue()) return fail(MA.GetStatus());
|
||||
Vector rhs(dofs.FreeDofCount());
|
||||
HighLow identity_metric, original_norm, solved_norm, old_norm;
|
||||
for (std::size_t j = 0; j < dofs.FreeDofCount(); ++j) {
|
||||
const auto i = dofs.FreeDofs()[j];
|
||||
detail::SolidHhtPairedForceIdentity identity;
|
||||
if (!detail::EvaluateSolidHhtHighLowForceIdentity(
|
||||
mass.Value(), i, Pair(MA.Value().high, MA.Value().low, i),
|
||||
candidate, old, p.alpha, &identity))
|
||||
return fail(
|
||||
Failure(domain, "solid-dynamic-nonfinite-residual",
|
||||
"Paired weighted force arithmetic is unsupported."));
|
||||
rhs[j] = Rounded(Negate(identity.solved));
|
||||
identity_metric = Maximum(identity_metric, identity.metric);
|
||||
original_norm = Maximum(original_norm, Abs(identity.original));
|
||||
solved_norm = Maximum(solved_norm, Abs(identity.solved));
|
||||
old_norm =
|
||||
Maximum(old_norm, Abs(Pair(old.residual, old.low.residual, i)));
|
||||
if (i == record.evidence.worst_full_dof)
|
||||
record.evidence.worst_weighted_terms = {
|
||||
Rounded(identity.original), Rounded(identity.solved),
|
||||
Rounded(Pair(old.residual, old.low.residual, i)),
|
||||
Rounded(identity.scale)};
|
||||
}
|
||||
if (!Valid(identity_metric) || !Valid(original_norm) ||
|
||||
!Valid(solved_norm) || !Valid(old_norm) ||
|
||||
Compare(identity_metric, HighLow(1e-12)) > 0)
|
||||
return fail(Failure(domain,
|
||||
"solid-dynamic-weighted-algebra-inconsistency",
|
||||
"Paired original and solved forces disagree beyond "
|
||||
"their uncancelled scale."));
|
||||
record.evidence.weighted_equivalence_metric = Rounded(identity_metric);
|
||||
record.evidence.weighted_residual_numerator = Rounded(original_norm);
|
||||
record.evidence.recombined_residual_numerator = Rounded(solved_norm);
|
||||
record.evidence.old_residual_numerator = Rounded(old_norm);
|
||||
for (std::size_t c = 0; c < 4; ++c) {
|
||||
if (!Valid(correction[c]) || !Valid(fresh_correction[c]))
|
||||
return fail(detail::SolidHhtPairedKinematicsFailure());
|
||||
record.evidence.correction_metrics[c] = Rounded(correction[c]);
|
||||
}
|
||||
record.evidence.correction_pass =
|
||||
CorrectionPass(correction) && CorrectionPass(fresh_correction);
|
||||
const auto update = CheckUpdates(old, candidate, p, h);
|
||||
if (!Valid(update))
|
||||
return fail(detail::SolidHhtPairedKinematicsFailure());
|
||||
record.evidence.update_metric = Rounded(update);
|
||||
record.evidence.update_pass = Compare(update, HighLow(1e-12)) <= 0;
|
||||
candidate.iterations.push_back(record.evidence);
|
||||
candidate.iteration_count = iteration;
|
||||
if (record.evidence.residual_pass && record.evidence.correction_pass &&
|
||||
record.evidence.update_pass) {
|
||||
status = state.CommitAcceptedFrame(candidate);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
record.stage = "accepted";
|
||||
record.outcome = "increment";
|
||||
state.RecordIteration(record);
|
||||
accepted = true;
|
||||
break;
|
||||
}
|
||||
state.RecordIteration(record);
|
||||
if (iteration == 100)
|
||||
return fail(Failure(
|
||||
domain, "solid-dynamic-nonconvergence",
|
||||
"Paired HHT endpoint did not converge in100 Newton solves."));
|
||||
auto effective =
|
||||
EffectiveTangent(mass.Value(), tangent.Value(), dofs, h, p);
|
||||
if (!effective.HasValue()) return fail(effective.GetStatus());
|
||||
auto reduced = partition.Partition(effective.Value(), dofs);
|
||||
if (!reduced.HasValue()) return fail(reduced.GetStatus());
|
||||
Vector du(dofs.FreeDofCount());
|
||||
if (dofs.FreeDofCount() != 0) {
|
||||
status = tangent_solver_.FactorizeFresh(reduced.Value().k_ff);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
status = tangent_solver_.SolveCorrection(rhs, du);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
}
|
||||
if (du.Size() != dofs.FreeDofCount())
|
||||
return fail(Failure(domain, "solid-dynamic-invalid-backend-result",
|
||||
"Tangent solve returned incompatible correction."));
|
||||
for (std::size_t j = 0; j < du.Size(); ++j)
|
||||
if (!std::isfinite(du[j]))
|
||||
return fail(Failure(domain, "solid-dynamic-invalid-backend-result",
|
||||
"Tangent solve returned nonfinite correction."));
|
||||
fresh_correction = FreshCorrectionMetrics(du, length.Value(), p);
|
||||
const auto before = candidate;
|
||||
for (std::size_t j = 0; j < du.Size(); ++j) {
|
||||
const auto i = dofs.FreeDofs()[j];
|
||||
const auto value =
|
||||
Add(Pair(candidate.displacement, candidate.low.displacement, i),
|
||||
HighLow(du[j] == 0 ? 0.0 : du[j]));
|
||||
if (!Valid(value))
|
||||
return fail(detail::SolidHhtPairedKinematicsFailure());
|
||||
Store(value, &candidate.displacement, &candidate.low.displacement, i);
|
||||
}
|
||||
status = SetRates(old, predictor.Value(), h, p, &candidate);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
correction =
|
||||
ObservedCorrectionMetrics(before, candidate, h, length.Value());
|
||||
}
|
||||
if (!accepted)
|
||||
return fail(Failure(domain, "solid-dynamic-nonconvergence",
|
||||
"No complete paired endpoint was accepted."));
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,182 @@
|
||||
#ifndef FESA_ANALYSIS_SOLID_HHT_FORCE_IDENTITY_H_
|
||||
#define FESA_ANALYSIS_SOLID_HHT_FORCE_IDENTITY_H_
|
||||
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
#include "solid_hht_high_low.h"
|
||||
|
||||
namespace fesa::detail {
|
||||
struct SolidHhtForceIdentity {
|
||||
double original{0};
|
||||
double solved{0};
|
||||
double scale{0};
|
||||
double defect{0};
|
||||
double metric{0};
|
||||
};
|
||||
|
||||
struct SolidHhtPairedForceIdentity {
|
||||
math_internal::HighLow original;
|
||||
math_internal::HighLow solved;
|
||||
math_internal::HighLow scale;
|
||||
math_internal::HighLow defect;
|
||||
math_internal::HighLow metric;
|
||||
};
|
||||
|
||||
/// @brief Keeps both physical parts and the actual old residual in the approved
|
||||
/// weighted identity. Its scale includes each uncancelled mass product.
|
||||
inline bool EvaluateSolidHhtHighLowForceIdentity(
|
||||
const SparseMatrix& mass, std::size_t row,
|
||||
math_internal::HighLow algorithmic_inertia,
|
||||
const SolidDynamicAcceptedFrameState& next,
|
||||
const SolidDynamicAcceptedFrameState& old, double alpha,
|
||||
SolidHhtPairedForceIdentity* result) {
|
||||
using namespace math_internal;
|
||||
if (!ArithmeticEnvironmentSupported() || row >= mass.Rows() ||
|
||||
!IsFinite(algorithmic_inertia) || !IsNormalized(algorithmic_inertia) ||
|
||||
!IsFinite(HighLow(alpha)))
|
||||
return false;
|
||||
for (const auto* frame : {&next, &old})
|
||||
if (frame->acceleration.Size() != mass.Columns() ||
|
||||
frame->low.acceleration.Size() != mass.Columns() ||
|
||||
frame->algorithmic_acceleration.Size() != mass.Columns() ||
|
||||
frame->low.algorithmic_acceleration.Size() != mass.Columns() ||
|
||||
frame->internal_force.Size() != mass.Rows() ||
|
||||
frame->low.internal_force.Size() != mass.Rows() ||
|
||||
frame->external_force.Size() != mass.Rows() ||
|
||||
frame->low.external_force.Size() != mass.Rows() ||
|
||||
frame->residual.Size() != mass.Rows() ||
|
||||
frame->low.residual.Size() != mass.Rows())
|
||||
return false;
|
||||
const HighLow a(alpha);
|
||||
const auto c = Add(HighLow(1), a);
|
||||
const HighLow f(next.internal_force[row], next.low.internal_force[row]);
|
||||
const HighLow F(next.external_force[row], next.low.external_force[row]);
|
||||
const HighLow old_f(old.internal_force[row], old.low.internal_force[row]);
|
||||
const HighLow old_F(old.external_force[row], old.low.external_force[row]);
|
||||
const HighLow r(next.residual[row], next.low.residual[row]);
|
||||
const HighLow old_r(old.residual[row], old.low.residual[row]);
|
||||
result->original =
|
||||
Subtract(Add(algorithmic_inertia, Multiply(c, Subtract(f, F))),
|
||||
Multiply(a, Subtract(old_f, old_F)));
|
||||
result->solved = Subtract(Multiply(c, r), Multiply(a, old_r));
|
||||
HighLow mass_scale;
|
||||
HighLow relation_action;
|
||||
for (std::size_t k = mass.RowOffsets()[row]; k < mass.RowOffsets()[row + 1];
|
||||
++k) {
|
||||
const auto j = mass.ColumnIndices()[k];
|
||||
const HighLow coefficient(mass.Values()[k]);
|
||||
const HighLow A(next.algorithmic_acceleration[j],
|
||||
next.low.algorithmic_acceleration[j]);
|
||||
const HighLow physical_a(next.acceleration[j], next.low.acceleration[j]);
|
||||
const HighLow old_a(old.acceleration[j], old.low.acceleration[j]);
|
||||
const auto relation =
|
||||
Add(Subtract(A, Multiply(c, physical_a)), Multiply(a, old_a));
|
||||
mass_scale =
|
||||
Add(mass_scale,
|
||||
Add(Add(Abs(Multiply(coefficient, A)),
|
||||
Multiply(c, Abs(Multiply(coefficient, physical_a)))),
|
||||
Multiply(Abs(a), Abs(Multiply(coefficient, old_a)))));
|
||||
relation_action =
|
||||
Add(relation_action, Abs(Multiply(coefficient, relation)));
|
||||
if (!IsFinite(mass_scale) || !IsFinite(relation_action)) return false;
|
||||
}
|
||||
result->scale = Add(
|
||||
Add(Add(mass_scale, relation_action), Multiply(c, Add(Abs(f), Abs(F)))),
|
||||
Add(Add(Multiply(Abs(a), Add(Abs(old_f), Abs(old_F))),
|
||||
Multiply(c, Abs(r))),
|
||||
Abs(Multiply(a, old_r))));
|
||||
result->defect = Abs(Subtract(result->original, result->solved));
|
||||
if (!IsFinite(result->original) || !IsFinite(result->solved) ||
|
||||
!IsFinite(result->scale) || !IsFinite(result->defect))
|
||||
return false;
|
||||
if (Compare(result->scale, HighLow(0)) == 0) {
|
||||
if (Compare(result->defect, HighLow(0)) != 0) return false;
|
||||
result->metric = HighLow(0);
|
||||
} else {
|
||||
result->metric = Divide(result->defect, result->scale);
|
||||
}
|
||||
return IsFinite(result->metric) && IsNormalized(result->metric);
|
||||
}
|
||||
|
||||
/// @brief Evaluates one original/equivalent HHT row, retaining old equilibrium
|
||||
/// error. Checks each scalar before it can enter an evidence maximum.
|
||||
inline bool EvaluateSolidHhtForceIdentity(
|
||||
const SparseMatrix& mass, std::size_t row, double algorithmic_inertia,
|
||||
const SolidDynamicAcceptedFrameState& next,
|
||||
const SolidDynamicAcceptedFrameState& old, double alpha,
|
||||
SolidHhtForceIdentity* result) {
|
||||
for (double x :
|
||||
{alpha, algorithmic_inertia, next.internal_force[row],
|
||||
next.external_force[row], old.internal_force[row],
|
||||
old.external_force[row], next.residual[row], old.residual[row]})
|
||||
if (!std::isfinite(x)) return false;
|
||||
result->original =
|
||||
algorithmic_inertia +
|
||||
(1 + alpha) * (next.internal_force[row] - next.external_force[row]) -
|
||||
alpha * (old.internal_force[row] - old.external_force[row]);
|
||||
result->solved = (1 + alpha) * next.residual[row] - alpha * old.residual[row];
|
||||
double mass_action_scale = 0;
|
||||
double relation_defect_action = 0;
|
||||
for (std::size_t entry = mass.RowOffsets()[row];
|
||||
entry < mass.RowOffsets()[row + 1]; ++entry) {
|
||||
const auto column = mass.ColumnIndices()[entry];
|
||||
const double coefficient = mass.Values()[entry];
|
||||
const double a = next.acceleration[column];
|
||||
const double A = next.algorithmic_acceleration[column];
|
||||
const double a_old = old.acceleration[column];
|
||||
const double relation_defect = A - (1 + alpha) * a + alpha * a_old;
|
||||
const double action_scale = std::abs(coefficient * A) +
|
||||
(1 + alpha) * std::abs(coefficient * a) +
|
||||
std::abs(alpha) * std::abs(coefficient * a_old);
|
||||
const double defect_action = std::abs(coefficient * relation_defect);
|
||||
for (double x : {a, A, a_old, relation_defect, action_scale, defect_action})
|
||||
if (!std::isfinite(x)) return false;
|
||||
mass_action_scale += action_scale;
|
||||
relation_defect_action += defect_action;
|
||||
}
|
||||
result->scale = mass_action_scale + relation_defect_action +
|
||||
(1 + alpha) * (std::abs(next.internal_force[row]) +
|
||||
std::abs(next.external_force[row])) +
|
||||
std::abs(alpha) * (std::abs(old.internal_force[row]) +
|
||||
std::abs(old.external_force[row])) +
|
||||
(1 + alpha) * std::abs(next.residual[row]) +
|
||||
std::abs(alpha * old.residual[row]);
|
||||
result->defect = std::abs(result->original - result->solved);
|
||||
for (double x : {result->original, result->solved, mass_action_scale,
|
||||
relation_defect_action, result->scale, result->defect})
|
||||
if (!std::isfinite(x)) return false;
|
||||
result->metric =
|
||||
result->scale == 0
|
||||
? (result->defect == 0 ? 0 : std::numeric_limits<double>::infinity())
|
||||
: result->defect / result->scale;
|
||||
return std::isfinite(result->metric);
|
||||
}
|
||||
/// @brief Finite checked failure-shadow accumulation, separate from physics.
|
||||
inline bool AccumulateSolidHhtForceShadow(double term, double product_error,
|
||||
double* sum, double* correction,
|
||||
double* absolute_sum) {
|
||||
if (!std::isfinite(term) || !std::isfinite(product_error) ||
|
||||
!std::isfinite(*sum) || !std::isfinite(*correction) ||
|
||||
!std::isfinite(*absolute_sum))
|
||||
return false;
|
||||
const double next = *sum + term;
|
||||
const double absolute_next = *absolute_sum + std::abs(term);
|
||||
const double error_next = *correction + product_error;
|
||||
const double lost = std::abs(*sum) >= std::abs(term) ? (*sum - next) + term
|
||||
: (term - next) + *sum;
|
||||
const double correction_next = error_next + lost;
|
||||
if (!std::isfinite(next) || !std::isfinite(absolute_next) ||
|
||||
!std::isfinite(error_next) || !std::isfinite(lost) ||
|
||||
!std::isfinite(correction_next))
|
||||
return false;
|
||||
*sum = next;
|
||||
*correction = correction_next;
|
||||
*absolute_sum = absolute_next;
|
||||
return true;
|
||||
}
|
||||
} // namespace fesa::detail
|
||||
#endif // FESA_ANALYSIS_SOLID_HHT_FORCE_IDENTITY_H_
|
||||
@@ -0,0 +1,190 @@
|
||||
#ifndef FESA_ANALYSIS_SOLID_HHT_HIGH_LOW_H_
|
||||
#define FESA_ANALYSIS_SOLID_HHT_HIGH_LOW_H_
|
||||
|
||||
#include "fesa/analysis/hht_time_integration.h"
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
#include "math/high_low_arithmetic.h"
|
||||
|
||||
namespace fesa::detail {
|
||||
/// @brief Keeps both physical parts of the fixed-increment predictor.
|
||||
struct SolidHhtPairedPredictor {
|
||||
Vector displacement{0};
|
||||
Vector displacement_low{0};
|
||||
Vector velocity{0};
|
||||
Vector velocity_low{0};
|
||||
};
|
||||
|
||||
/// @brief Owns physical and auxiliary rates without collapsing either part.
|
||||
struct SolidHhtPairedRates {
|
||||
Vector velocity{0};
|
||||
Vector velocity_low{0};
|
||||
Vector acceleration{0};
|
||||
Vector acceleration_low{0};
|
||||
Vector algorithmic_acceleration{0};
|
||||
Vector algorithmic_acceleration_low{0};
|
||||
};
|
||||
|
||||
struct SolidHhtPairedVector {
|
||||
Vector high{0};
|
||||
Vector low{0};
|
||||
};
|
||||
|
||||
inline Status SolidHhtPairedKinematicsFailure() {
|
||||
return Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError,
|
||||
"solid-dynamic-invalid-paired-kinematics",
|
||||
{},
|
||||
"DYNAMIC",
|
||||
{},
|
||||
"Incomplete, unnormalized or nonfinite paired HHT "
|
||||
"kinematics."}});
|
||||
}
|
||||
|
||||
inline bool ValidSolidHhtPair(const Vector& high, const Vector& low,
|
||||
std::size_t size) {
|
||||
if (high.Size() != size || low.Size() != size) return false;
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
const math_internal::HighLow value(high[i], low[i]);
|
||||
if (!math_internal::IsFinite(value) || !math_internal::IsNormalized(value))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ValidSolidHhtParameters(double h, const HhtParameters& p) {
|
||||
if (!math_internal::ArithmeticEnvironmentSupported()) return false;
|
||||
const auto expected = MakeHhtParameters(p.alpha);
|
||||
if (!expected.HasValue() || p.beta != expected.Value().beta ||
|
||||
p.gamma != expected.Value().gamma)
|
||||
return false;
|
||||
const math_internal::HighLow step(h);
|
||||
const auto square = math_internal::Multiply(step, step);
|
||||
return math_internal::IsFinite(step) &&
|
||||
math_internal::Compare(step, math_internal::HighLow(0)) > 0 &&
|
||||
math_internal::IsFinite(square) &&
|
||||
math_internal::Compare(square, math_internal::HighLow(0)) > 0;
|
||||
}
|
||||
|
||||
/// @brief Uses the authoritative constant Float64 mass coefficients without
|
||||
/// collapsing either physical vector part or the ordered row accumulation.
|
||||
inline Result<SolidHhtPairedVector> MultiplySolidHhtMassHighLow(
|
||||
const SparseMatrix& mass, const Vector& high, const Vector& low) {
|
||||
using namespace math_internal;
|
||||
if (!ArithmeticEnvironmentSupported() || mass.Rows() != mass.Columns() ||
|
||||
!ValidSolidHhtPair(high, low, mass.Columns()))
|
||||
return Result<SolidHhtPairedVector>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
SolidHhtPairedVector result{Vector(mass.Rows()), Vector(mass.Rows())};
|
||||
for (std::size_t row = 0; row < mass.Rows(); ++row) {
|
||||
HighLow sum;
|
||||
for (std::size_t k = mass.RowOffsets()[row]; k < mass.RowOffsets()[row + 1];
|
||||
++k) {
|
||||
const auto col = mass.ColumnIndices()[k];
|
||||
sum = Add(sum, Multiply(HighLow(mass.Values()[k]),
|
||||
HighLow(high[col], low[col])));
|
||||
if (!IsFinite(sum) || !IsNormalized(sum))
|
||||
return Result<SolidHhtPairedVector>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
}
|
||||
result.high[row] = sum.high;
|
||||
result.low[row] = sum.low;
|
||||
}
|
||||
return Result<SolidHhtPairedVector>::Success(std::move(result));
|
||||
}
|
||||
|
||||
/// @brief Applies the unchanged Newmark predictor to the full physical state.
|
||||
inline Result<SolidHhtPairedPredictor> PredictSolidHhtHighLow(
|
||||
const SolidDynamicAcceptedFrameState& old, double h,
|
||||
const HhtParameters& p) {
|
||||
using namespace math_internal;
|
||||
const auto size = old.displacement.Size();
|
||||
if (!ValidSolidHhtPair(old.displacement, old.low.displacement, size) ||
|
||||
!ValidSolidHhtPair(old.velocity, old.low.velocity, size) ||
|
||||
!ValidSolidHhtPair(old.algorithmic_acceleration,
|
||||
old.low.algorithmic_acceleration, size) ||
|
||||
!ValidSolidHhtParameters(h, p))
|
||||
return Result<SolidHhtPairedPredictor>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
SolidHhtPairedPredictor result{Vector(size), Vector(size), Vector(size),
|
||||
Vector(size)};
|
||||
const auto hh = Multiply(HighLow(h), HighLow(h));
|
||||
const auto u_coefficient =
|
||||
Multiply(hh, Subtract(HighLow(.5), HighLow(p.beta)));
|
||||
const auto v_coefficient =
|
||||
Multiply(HighLow(h), Subtract(HighLow(1), HighLow(p.gamma)));
|
||||
if (!IsFinite(u_coefficient) || !IsFinite(v_coefficient))
|
||||
return Result<SolidHhtPairedPredictor>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
const HighLow u(old.displacement[i], old.low.displacement[i]);
|
||||
const HighLow v(old.velocity[i], old.low.velocity[i]);
|
||||
const HighLow A(old.algorithmic_acceleration[i],
|
||||
old.low.algorithmic_acceleration[i]);
|
||||
const auto up =
|
||||
Add(Add(u, Multiply(HighLow(h), v)), Multiply(u_coefficient, A));
|
||||
const auto vp = Add(v, Multiply(v_coefficient, A));
|
||||
if (!IsFinite(up) || !IsNormalized(up) || !IsFinite(vp) ||
|
||||
!IsNormalized(vp))
|
||||
return Result<SolidHhtPairedPredictor>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
result.displacement[i] = up.high;
|
||||
result.displacement_low[i] = up.low;
|
||||
result.velocity[i] = vp.high;
|
||||
result.velocity_low[i] = vp.low;
|
||||
}
|
||||
return Result<SolidHhtPairedPredictor>::Success(std::move(result));
|
||||
}
|
||||
|
||||
/// @brief Recovers A, v and physical a from the paired displacement difference.
|
||||
inline Result<SolidHhtPairedRates> EvaluateSolidHhtHighLowRates(
|
||||
const Vector& high, const Vector& low,
|
||||
const SolidHhtPairedPredictor& predictor, const Vector& old_acceleration,
|
||||
const Vector& old_acceleration_low, double h, const HhtParameters& p) {
|
||||
using namespace math_internal;
|
||||
const auto size = high.Size();
|
||||
if (!ValidSolidHhtPair(high, low, size) ||
|
||||
!ValidSolidHhtPair(predictor.displacement, predictor.displacement_low,
|
||||
size) ||
|
||||
!ValidSolidHhtPair(predictor.velocity, predictor.velocity_low, size) ||
|
||||
!ValidSolidHhtPair(old_acceleration, old_acceleration_low, size) ||
|
||||
!ValidSolidHhtParameters(h, p))
|
||||
return Result<SolidHhtPairedRates>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
SolidHhtPairedRates result{Vector(size), Vector(size), Vector(size),
|
||||
Vector(size), Vector(size), Vector(size)};
|
||||
const auto denominator =
|
||||
Multiply(Multiply(HighLow(p.beta), HighLow(h)), HighLow(h));
|
||||
const auto velocity_coefficient = Multiply(HighLow(p.gamma), HighLow(h));
|
||||
const auto physical_coefficient = Add(HighLow(1), HighLow(p.alpha));
|
||||
if (!IsFinite(denominator) || Compare(denominator, HighLow(0)) <= 0 ||
|
||||
!IsFinite(velocity_coefficient) || !IsFinite(physical_coefficient))
|
||||
return Result<SolidHhtPairedRates>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
const auto A = Divide(Subtract(HighLow(high[i], low[i]),
|
||||
HighLow(predictor.displacement[i],
|
||||
predictor.displacement_low[i])),
|
||||
denominator);
|
||||
const auto v =
|
||||
Add(HighLow(predictor.velocity[i], predictor.velocity_low[i]),
|
||||
Multiply(velocity_coefficient, A));
|
||||
const auto a = Divide(
|
||||
Add(A, Multiply(HighLow(p.alpha),
|
||||
HighLow(old_acceleration[i], old_acceleration_low[i]))),
|
||||
physical_coefficient);
|
||||
if (!IsFinite(A) || !IsNormalized(A) || !IsFinite(v) || !IsNormalized(v) ||
|
||||
!IsFinite(a) || !IsNormalized(a))
|
||||
return Result<SolidHhtPairedRates>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
result.algorithmic_acceleration[i] = A.high;
|
||||
result.algorithmic_acceleration_low[i] = A.low;
|
||||
result.velocity[i] = v.high;
|
||||
result.velocity_low[i] = v.low;
|
||||
result.acceleration[i] = a.high;
|
||||
result.acceleration_low[i] = a.low;
|
||||
}
|
||||
return Result<SolidHhtPairedRates>::Success(std::move(result));
|
||||
}
|
||||
} // namespace fesa::detail
|
||||
#endif // FESA_ANALYSIS_SOLID_HHT_HIGH_LOW_H_
|
||||
@@ -0,0 +1,217 @@
|
||||
#ifndef FESA_ANALYSIS_SOLID_HHT_REPRESENTABILITY_H_
|
||||
#define FESA_ANALYSIS_SOLID_HHT_REPRESENTABILITY_H_
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
namespace fesa::detail {
|
||||
struct SolidHhtRepresentableNeighbor {
|
||||
bool found{false};
|
||||
std::size_t full_dof{0};
|
||||
double displacement{0};
|
||||
double delta{0};
|
||||
double predicted_metric{0};
|
||||
};
|
||||
/// @brief Predicts a strictly improving one-ULP neighbor using separate
|
||||
/// physical inertia/internal-force actions and every original free score.
|
||||
/// This selects a trial only; callers must recompute actual endpoint gates.
|
||||
inline bool SelectSolidHhtRepresentableNeighbor(
|
||||
const SparseMatrix& mass, const SparseMatrix& tangent,
|
||||
const std::vector<std::size_t>& free_dofs,
|
||||
const SolidDynamicAcceptedFrameState& frame, double h, double alpha,
|
||||
double beta, SolidHhtRepresentableNeighbor* selected,
|
||||
std::vector<SolidHhtRepresentableNeighbor>* ranked = nullptr) {
|
||||
*selected = {};
|
||||
if (ranked != nullptr) ranked->clear();
|
||||
const auto size = mass.Rows();
|
||||
if (tangent.Rows() != size || mass.Columns() != size ||
|
||||
tangent.Columns() != size || mass.RowOffsets() != tangent.RowOffsets() ||
|
||||
mass.ColumnIndices() != tangent.ColumnIndices() ||
|
||||
frame.displacement.Size() != size || frame.inertia_force.Size() != size ||
|
||||
frame.internal_force.Size() != size ||
|
||||
frame.external_force.Size() != size || frame.residual.Size() != size ||
|
||||
frame.residual_scale.Size() != size)
|
||||
return false;
|
||||
const double inertia_factor = 1 / ((1 + alpha) * beta * h * h);
|
||||
if (!std::isfinite(inertia_factor) || inertia_factor <= 0) return false;
|
||||
const auto score = [](double ma, double f, double F, double* metric) {
|
||||
const double scale = std::abs(ma) + std::abs(f) + std::abs(F);
|
||||
const double r = ma + f - F;
|
||||
if (!std::isfinite(scale) || !std::isfinite(r)) return false;
|
||||
*metric = scale == 0
|
||||
? (r == 0 ? 0 : std::numeric_limits<double>::infinity())
|
||||
: std::abs(r) / scale;
|
||||
return std::isfinite(*metric);
|
||||
};
|
||||
std::vector<bool> is_free(size, false);
|
||||
std::vector<double> scores(size, 0);
|
||||
double best = 0;
|
||||
for (auto i : free_dofs) {
|
||||
if (i >= size || !std::isfinite(frame.displacement[i]) ||
|
||||
!score(frame.inertia_force[i], frame.internal_force[i],
|
||||
frame.external_force[i], &scores[i]))
|
||||
return false;
|
||||
is_free[i] = true;
|
||||
best = (std::max)(best, scores[i]);
|
||||
}
|
||||
struct Action {
|
||||
std::size_t row;
|
||||
double inertia;
|
||||
double internal;
|
||||
};
|
||||
std::vector<std::vector<Action>> columns(size);
|
||||
for (auto i : free_dofs)
|
||||
for (std::size_t k = mass.RowOffsets()[i]; k < mass.RowOffsets()[i + 1];
|
||||
++k) {
|
||||
const auto j = mass.ColumnIndices()[k];
|
||||
if (!is_free[j]) continue;
|
||||
const double inertia = mass.Values()[k] * inertia_factor;
|
||||
const double internal = tangent.Values()[k];
|
||||
if (!std::isfinite(inertia) || !std::isfinite(internal)) return false;
|
||||
columns[j].push_back({i, inertia, internal});
|
||||
}
|
||||
const double original_metric = best;
|
||||
auto predicted = scores;
|
||||
for (auto j : free_dofs)
|
||||
for (const double direction : {-std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity()}) {
|
||||
const double proposed = std::nextafter(frame.displacement[j], direction);
|
||||
const double delta = proposed - frame.displacement[j];
|
||||
if (!std::isfinite(proposed) || !std::isfinite(delta) || delta == 0)
|
||||
continue;
|
||||
predicted = scores;
|
||||
for (const auto& action : columns[j]) {
|
||||
const auto i = action.row;
|
||||
const double ma = frame.inertia_force[i] + action.inertia * delta;
|
||||
const double f = frame.internal_force[i] + action.internal * delta;
|
||||
if (!score(ma, f, frame.external_force[i], &predicted[i])) return false;
|
||||
}
|
||||
double metric = 0;
|
||||
for (auto i : free_dofs) metric = (std::max)(metric, predicted[i]);
|
||||
if (ranked != nullptr && metric < original_metric)
|
||||
ranked->push_back({true, j, proposed, delta, metric});
|
||||
// Stable full-DOF/direction ordering resolves ties without a floor.
|
||||
if (metric < best) {
|
||||
best = metric;
|
||||
*selected = {true, j, proposed, delta, metric};
|
||||
}
|
||||
}
|
||||
if (ranked != nullptr)
|
||||
std::stable_sort(ranked->begin(), ranked->end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return a.predicted_metric < b.predicted_metric;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/// @brief Keeps complete eligible failed Newton entries for a fixed increment.
|
||||
class SolidHhtNewtonHistory {
|
||||
public:
|
||||
bool Capture(const Vector& u, const std::vector<bool>& holding_mask,
|
||||
const std::array<double, 4>& incoming_metrics,
|
||||
std::uint32_t iteration, bool physical_pass) {
|
||||
if (!Valid(u, holding_mask, incoming_metrics)) return false;
|
||||
if (!physical_pass &&
|
||||
std::all_of(incoming_metrics.begin(), incoming_metrics.end(),
|
||||
[](double metric) { return metric <= 1e-10; }))
|
||||
entries_.push_back({u, holding_mask, incoming_metrics, iteration});
|
||||
return true;
|
||||
}
|
||||
bool Match(const Vector& proposed_u,
|
||||
const std::vector<bool>& next_holding_mask,
|
||||
const std::array<double, 4>& fresh_metrics, bool physical_pass,
|
||||
SolidDynamicNewtonRepeatEvidence* evidence) const {
|
||||
*evidence = {};
|
||||
evidence->history_entry_count = entries_.size();
|
||||
if (!Valid(proposed_u, next_holding_mask, fresh_metrics)) return false;
|
||||
// A physically converged prospective endpoint must reach the normal gate.
|
||||
if (physical_pass ||
|
||||
!std::all_of(fresh_metrics.begin(), fresh_metrics.end(),
|
||||
[](double metric) { return metric <= 1e-10; }))
|
||||
return true;
|
||||
for (const auto& entry : entries_) {
|
||||
evidence->changed_translations = 0;
|
||||
evidence->changed_holding_mask = 0;
|
||||
evidence->changed_correction_metrics = 0;
|
||||
for (std::size_t i = 0; i < proposed_u.Size(); ++i)
|
||||
evidence->changed_translations += proposed_u[i] != entry.u[i];
|
||||
for (std::size_t i = 0; i < next_holding_mask.size(); ++i)
|
||||
evidence->changed_holding_mask +=
|
||||
next_holding_mask[i] != entry.holding_mask[i];
|
||||
for (std::size_t i = 0; i < 4; ++i)
|
||||
evidence->changed_correction_metrics +=
|
||||
fresh_metrics[i] != entry.incoming_metrics[i];
|
||||
// Numeric correction magnitudes remain evidence. Their finite/PASS
|
||||
// controls, rather than their exact magnitudes, affect the transition.
|
||||
if (evidence->changed_translations == 0 &&
|
||||
evidence->changed_holding_mask == 0) {
|
||||
evidence->matched_entry_iteration = entry.iteration;
|
||||
evidence->repeat = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::size_t Size() const { return entries_.size(); }
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
Vector u;
|
||||
std::vector<bool> holding_mask;
|
||||
std::array<double, 4> incoming_metrics;
|
||||
std::uint32_t iteration;
|
||||
};
|
||||
bool Valid(const Vector& u, const std::vector<bool>& mask,
|
||||
const std::array<double, 4>& metrics) const {
|
||||
if (mask.size() > u.Size() ||
|
||||
(!entries_.empty() &&
|
||||
(u.Size() != entries_.front().u.Size() ||
|
||||
mask.size() != entries_.front().holding_mask.size())))
|
||||
return false;
|
||||
for (std::size_t i = 0; i < u.Size(); ++i)
|
||||
if (!std::isfinite(u[i])) return false;
|
||||
for (double metric : metrics)
|
||||
if (!std::isfinite(metric) || metric < 0) return false;
|
||||
return true;
|
||||
}
|
||||
std::vector<Entry> entries_;
|
||||
};
|
||||
/// @brief Checks the fresh complete Newton trial before representable fallback.
|
||||
/// Historical scores are deliberately absent: a lower half of a two-cycle
|
||||
/// still needs actual proposed-step evaluation. False keeps common Newton
|
||||
/// apply.
|
||||
template <class Newton, class Neighbor>
|
||||
Result<bool> TrySolidHhtActualStep(
|
||||
bool residual_pass, const std::array<double, 4>& fresh_corrections,
|
||||
Newton newton, Neighbor neighbor) {
|
||||
if (residual_pass || !std::all_of(fresh_corrections.begin(),
|
||||
fresh_corrections.end(), [](double metric) {
|
||||
return std::isfinite(metric) &&
|
||||
metric <= 1e-10;
|
||||
}))
|
||||
return Result<bool>::Success(false);
|
||||
auto keep_newton = newton();
|
||||
if (!keep_newton.HasValue())
|
||||
return Result<bool>::Failure(keep_newton.GetStatus());
|
||||
if (keep_newton.Value()) return Result<bool>::Success(false);
|
||||
return neighbor();
|
||||
}
|
||||
/// @brief Tries ranked predictions until actual endpoint evaluation improves.
|
||||
/// Evaluator failure is terminal; a rejected prediction does not discard later
|
||||
/// candidates, and success means only that a local trial improved.
|
||||
template <class Evaluate>
|
||||
Result<bool> TrySolidHhtRankedNeighbors(
|
||||
const std::vector<SolidHhtRepresentableNeighbor>& ranked,
|
||||
Evaluate evaluate) {
|
||||
for (const auto& neighbor : ranked) {
|
||||
auto actual = evaluate(neighbor);
|
||||
if (!actual.HasValue()) return Result<bool>::Failure(actual.GetStatus());
|
||||
if (actual.Value()) return Result<bool>::Success(true);
|
||||
}
|
||||
return Result<bool>::Success(false);
|
||||
}
|
||||
} // namespace fesa::detail
|
||||
#endif // FESA_ANALYSIS_SOLID_HHT_REPRESENTABILITY_H_
|
||||
Reference in New Issue
Block a user