feat(hex8): integrate paired HHT dynamic state and bounded verification

This commit is contained in:
KOKO\Mimi
2026-10-10 00:36:16 +09:00
parent c7c3c8017f
commit 338902974c
17 changed files with 4291 additions and 11 deletions
@@ -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, &current_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_
+190
View File
@@ -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_