feat(analysis): implement Hex8 solid static procedures
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
#include "fesa/analysis/solid_static_procedure.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/assembly/load_assembler.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/assembly/sparse_assembler.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"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
constexpr std::uint32_t kMaximumIterations = 100U;
|
||||
constexpr double kRoundoffBudget =
|
||||
512.0 * std::numeric_limits<double>::epsilon();
|
||||
|
||||
struct RuntimeSolid {
|
||||
const Hex8Element* element;
|
||||
std::vector<std::size_t> scatter;
|
||||
};
|
||||
struct SolidAssembly {
|
||||
Vector force;
|
||||
SparseMatrix tangent;
|
||||
std::vector<SolidIntegrationPointRow> points;
|
||||
double energy;
|
||||
};
|
||||
|
||||
/// @brief Attaches immutable source-step context to a procedure failure.
|
||||
Status Failure(const Domain& domain, const char* code, const char* message,
|
||||
FailureCategory category = FailureCategory::kSolver) {
|
||||
return Status::Failure(category, {{Severity::kError,
|
||||
code,
|
||||
{domain.SourcePath(), 0U},
|
||||
"STATIC",
|
||||
domain.SourceContentIdentity(),
|
||||
message}});
|
||||
}
|
||||
/// @brief Preserves kernel source/entity evidence for failed trial recovery.
|
||||
Status TrialFailure(const Status& status) {
|
||||
return Status::Failure(FailureCategory::kSolver, status.Diagnostics());
|
||||
}
|
||||
/// @brief Returns component products with uncancelled arithmetic scales.
|
||||
std::array<double, 3> Cross(const std::array<double, 3>& a,
|
||||
const std::array<double, 3>& b,
|
||||
std::array<double, 3>* scale = nullptr) {
|
||||
std::array<double, 3> result{};
|
||||
for (std::size_t i = 0U; i < 3U; ++i) {
|
||||
const auto j = (i + 1U) % 3U;
|
||||
const auto k = (i + 2U) % 3U;
|
||||
result[i] = a[j] * b[k] - a[k] * b[j];
|
||||
if (scale != nullptr)
|
||||
(*scale)[i] += std::abs(a[j] * b[k]) + std::abs(a[k] * b[j]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/// @brief Tests a zero identity at its uncancelled same-unit operation scale.
|
||||
bool RoundoffZero(double value, double scale) {
|
||||
return std::isfinite(value) && std::isfinite(scale) &&
|
||||
(scale == 0.0 ? value == 0.0
|
||||
: std::abs(value) <= kRoundoffBudget * scale);
|
||||
}
|
||||
/// @brief Derives source DIRECT factors or the existing ordinary STATIC path.
|
||||
Result<std::vector<LoadFactorIncrement>> Factors(const Domain& domain) {
|
||||
const auto& step = domain.Steps()[0];
|
||||
if (!step.IsDirectStatic()) return BuildFixedLoadFactorSequence(step);
|
||||
const auto count = step.IncrementCount();
|
||||
if (count == 0U || count > step.MaximumIncrements() ||
|
||||
!std::isfinite(step.InitialIncrement()) ||
|
||||
!(step.InitialIncrement() > 0.0) || !std::isfinite(step.TimePeriod()) ||
|
||||
!(step.TimePeriod() > 0.0) ||
|
||||
step.InitialIncrement() * static_cast<double>(count) != step.TimePeriod())
|
||||
return Result<std::vector<LoadFactorIncrement>>::Failure(Failure(
|
||||
domain, "invalid-solid-static-schedule",
|
||||
"DIRECT static requires a finite positive integer fixed schedule.",
|
||||
FailureCategory::kInput));
|
||||
std::vector<LoadFactorIncrement> factors;
|
||||
factors.reserve(count);
|
||||
double previous = 0.0;
|
||||
for (std::uint32_t n = 1U; n <= count; ++n) {
|
||||
const double factor = n == count
|
||||
? 1.0
|
||||
: static_cast<double>(n) *
|
||||
step.InitialIncrement() / step.TimePeriod();
|
||||
if (!std::isfinite(factor) || !(factor > previous) || factor > 1.0)
|
||||
return Result<std::vector<LoadFactorIncrement>>::Failure(
|
||||
Failure(domain, "invalid-solid-static-schedule",
|
||||
"DIRECT static factors must strictly increase to one.",
|
||||
FailureCategory::kInput));
|
||||
factors.push_back({n, factor, n == count});
|
||||
previous = factor;
|
||||
if (n == count) break; // Avoid unsigned wrap at the source limit.
|
||||
}
|
||||
return Result<std::vector<LoadFactorIncrement>>::Success(std::move(factors));
|
||||
}
|
||||
/// @brief Builds index-owned trials, then reduces all contributions stably.
|
||||
Result<SolidAssembly> Assemble(const std::vector<RuntimeSolid>& solids,
|
||||
const DofManager& dofs,
|
||||
const Vector& displacement, bool nonlinear,
|
||||
const ParallelFor& parallel) {
|
||||
std::vector<std::optional<Result<Hex8Response>>> responses(solids.size());
|
||||
parallel.Execute(solids.size(), [&](std::size_t e) {
|
||||
Vector local{24U};
|
||||
for (std::size_t i = 0U; i < 24U; ++i)
|
||||
local[i] = displacement[solids[e].scatter[i]];
|
||||
responses[e] = nonlinear ? solids[e].element->EvaluateNonlinear(local)
|
||||
: solids[e].element->EvaluateLinear(local);
|
||||
});
|
||||
Vector force{dofs.FullDofCount()};
|
||||
std::vector<CooContribution> contributions;
|
||||
contributions.reserve(solids.size() * 24U * 24U);
|
||||
std::vector<SolidIntegrationPointRow> points;
|
||||
points.reserve(solids.size() * 8U);
|
||||
double energy = 0.0;
|
||||
for (std::size_t e = 0U; e < solids.size(); ++e) {
|
||||
if (!responses[e]->HasValue())
|
||||
return Result<SolidAssembly>::Failure(
|
||||
TrialFailure(responses[e]->GetStatus()));
|
||||
const auto& response = responses[e]->Value();
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
force[solids[e].scatter[i]] += response.internal_force[i];
|
||||
for (std::size_t j = 0U; j < 24U; ++j)
|
||||
contributions.push_back({solids[e].scatter[i], solids[e].scatter[j],
|
||||
response.tangent(i, j), e, 24U * i + j});
|
||||
}
|
||||
energy += response.strain_energy;
|
||||
points.insert(points.end(), response.points.begin(), response.points.end());
|
||||
}
|
||||
auto tangent =
|
||||
SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(),
|
||||
std::move(contributions), dofs.GetSparsePattern());
|
||||
if (!tangent.HasValue())
|
||||
return Result<SolidAssembly>::Failure(tangent.GetStatus());
|
||||
return Result<SolidAssembly>::Success({std::move(force),
|
||||
std::move(tangent.Value()),
|
||||
std::move(points), energy});
|
||||
}
|
||||
/// @brief Computes physical residual, projection and both moment-arm
|
||||
/// identities.
|
||||
Status CompleteFrame(const Domain& domain, const DofManager& dofs,
|
||||
bool nonlinear, const SparseMatrix& stiffness,
|
||||
SolidStaticFrame& frame) {
|
||||
const std::size_t size = dofs.FullDofCount();
|
||||
std::vector<bool> constrained(size, false);
|
||||
for (auto i : dofs.ConstrainedDofs()) constrained[i] = true;
|
||||
frame.residual = Vector{size};
|
||||
frame.reaction = Vector{size};
|
||||
for (std::size_t i = 0U; i < size; ++i) {
|
||||
frame.residual[i] = frame.internal_force[i] - frame.external_force[i];
|
||||
frame.reaction[i] = constrained[i] ? frame.residual[i] : 0.0;
|
||||
if (!std::isfinite(frame.displacement[i]) ||
|
||||
!std::isfinite(frame.internal_force[i]) ||
|
||||
!std::isfinite(frame.external_force[i]) ||
|
||||
!std::isfinite(frame.residual[i]))
|
||||
return Failure(domain, "nonfinite-solid-static-recovery",
|
||||
"Static full-space quantities must remain finite.");
|
||||
}
|
||||
if (!std::isfinite(frame.physical_strain_energy))
|
||||
return Failure(domain, "nonfinite-solid-static-recovery",
|
||||
"Recovered static strain energy must be finite.");
|
||||
std::array<double, 3> force_scale{};
|
||||
std::array<double, 3> reference_moment_scale{};
|
||||
std::array<double, 3> current_moment_scale{};
|
||||
std::array<double, 3> identity_moment_scale{};
|
||||
std::array<double, 3> free_force_sum{};
|
||||
std::array<double, 6> balance_scale{};
|
||||
Vector arithmetic_scale{size};
|
||||
// Linear K0 multiplication can cancel large translations within each row.
|
||||
// Use those original products, not the small resulting internal force.
|
||||
if (!nonlinear) {
|
||||
for (std::size_t i = 0U; i < size; ++i)
|
||||
for (std::size_t k = stiffness.RowOffsets()[i];
|
||||
k < stiffness.RowOffsets()[i + 1U]; ++k)
|
||||
arithmetic_scale[i] +=
|
||||
std::abs(stiffness.Values()[k] *
|
||||
frame.displacement[stiffness.ColumnIndices()[k]]);
|
||||
for (auto i : dofs.FreeDofs())
|
||||
if (!RoundoffZero(
|
||||
frame.residual[i],
|
||||
arithmetic_scale[i] + std::abs(frame.external_force[i])))
|
||||
return Failure(
|
||||
domain, "solid-linear-residual-failed",
|
||||
"Linear free residual failed its uncancelled roundoff check.");
|
||||
}
|
||||
for (std::size_t a = 0U; a < domain.Nodes().size(); ++a) {
|
||||
const auto& reference = domain.Nodes()[a].coordinates;
|
||||
std::array<double, 3> current{};
|
||||
std::array<double, 3> u{};
|
||||
std::array<double, 3> f{};
|
||||
std::array<double, 3> e{};
|
||||
std::array<double, 3> applied{};
|
||||
for (std::size_t i = 0U; i < 3U; ++i) {
|
||||
const auto full = 3U * a + i;
|
||||
u[i] = frame.displacement[full];
|
||||
current[i] = reference[i] + u[i];
|
||||
f[i] = frame.internal_force[full];
|
||||
e[i] = constrained[full] ? 0.0 : frame.residual[full];
|
||||
applied[i] = frame.external_force[full] + frame.reaction[full];
|
||||
frame.balance.internal_force_sum[i] += f[i];
|
||||
free_force_sum[i] += e[i];
|
||||
force_scale[i] += nonlinear ? std::abs(f[i]) : arithmetic_scale[full];
|
||||
frame.global_equilibrium[i] += applied[i];
|
||||
balance_scale[i] += std::abs(frame.external_force[full]) +
|
||||
std::abs(frame.reaction[full]) + std::abs(f[i]) +
|
||||
std::abs(e[i]);
|
||||
}
|
||||
const auto reference_moment = Cross(reference, f, &reference_moment_scale);
|
||||
const auto current_moment = Cross(current, f, ¤t_moment_scale);
|
||||
const auto free_moment = Cross(current, e, &identity_moment_scale);
|
||||
const auto linear_moment = Cross(u, f, &identity_moment_scale);
|
||||
std::array<double, 3> applied_scale{};
|
||||
const auto applied_moment = Cross(current, applied, &applied_scale);
|
||||
for (std::size_t i = 0U; i < 3U; ++i) {
|
||||
frame.balance.reference_internal_moment[i] += reference_moment[i];
|
||||
frame.balance.current_internal_moment[i] += current_moment[i];
|
||||
frame.balance.free_residual_moment[i] += free_moment[i];
|
||||
frame.balance.linearization_moment[i] += linear_moment[i];
|
||||
frame.global_equilibrium[3U + i] += applied_moment[i];
|
||||
balance_scale[3U + i] += applied_scale[i];
|
||||
if (!nonlinear) {
|
||||
const auto j = (i + 1U) % 3U;
|
||||
const auto k = (i + 2U) % 3U;
|
||||
reference_moment_scale[i] +=
|
||||
std::abs(reference[j]) * arithmetic_scale[3U * a + k] +
|
||||
std::abs(reference[k]) * arithmetic_scale[3U * a + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
frame.balance.internal_force_scale = force_scale;
|
||||
frame.balance.reference_moment_scale = reference_moment_scale;
|
||||
frame.balance.current_moment_scale = current_moment_scale;
|
||||
for (std::size_t i = 0U; i < 3U; ++i) {
|
||||
const double physical_moment =
|
||||
nonlinear ? frame.balance.current_internal_moment[i]
|
||||
: frame.balance.reference_internal_moment[i];
|
||||
const double moment_scale =
|
||||
nonlinear ? current_moment_scale[i] : reference_moment_scale[i];
|
||||
if (!RoundoffZero(frame.balance.internal_force_sum[i], force_scale[i]) ||
|
||||
!RoundoffZero(physical_moment, moment_scale))
|
||||
return Failure(
|
||||
domain, "solid-static-internal-balance-failed",
|
||||
"Internal force or appropriate moment-arm identity failed.");
|
||||
const double force_identity = frame.global_equilibrium[i] -
|
||||
frame.balance.internal_force_sum[i] +
|
||||
free_force_sum[i];
|
||||
const double moment_identity = frame.global_equilibrium[3U + i] -
|
||||
frame.balance.current_internal_moment[i] +
|
||||
frame.balance.free_residual_moment[i];
|
||||
if (!RoundoffZero(force_identity, balance_scale[i]) ||
|
||||
!RoundoffZero(moment_identity, balance_scale[3U + i] +
|
||||
current_moment_scale[i] +
|
||||
identity_moment_scale[i]))
|
||||
return Failure(
|
||||
domain, "solid-static-residual-balance-failed",
|
||||
"Applied/support balance does not explain the free residual.");
|
||||
if (!std::isfinite(frame.global_equilibrium[i]) ||
|
||||
!std::isfinite(frame.global_equilibrium[3U + i]))
|
||||
return Failure(domain, "nonfinite-solid-static-recovery",
|
||||
"Static current-position balance must remain finite.");
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SolidStaticProcedureExecutor::SolidStaticProcedureExecutor(
|
||||
const ParallelFor& parallel_for, LinearSolver& linear_solver,
|
||||
TangentSolver& tangent_solver)
|
||||
: parallel_for_(parallel_for),
|
||||
linear_solver_(linear_solver),
|
||||
tangent_solver_(tangent_solver) {}
|
||||
|
||||
Status SolidStaticProcedureExecutor::ExecuteInto(
|
||||
const Domain& domain, SolidStaticState& state) const {
|
||||
if (state.initialized_)
|
||||
return Failure(domain, "solid-static-observer-not-fresh",
|
||||
"Execution requires a fresh static observer.");
|
||||
if (!domain.IsSolid() || domain.Frequency() != nullptr ||
|
||||
domain.Dynamic() != nullptr || domain.Steps().Size() != 1U)
|
||||
return Failure(domain, "invalid-solid-static-procedure",
|
||||
"Solid static execution requires one Solid STATIC step.",
|
||||
FailureCategory::kModel);
|
||||
auto model = AnalysisModel::Create(domain);
|
||||
if (!model.HasValue()) return model.GetStatus();
|
||||
std::vector<std::unique_ptr<Element>> elements;
|
||||
ElementView view;
|
||||
for (auto e : model.Value().ActiveElements()) {
|
||||
auto element = ElementFactory{}.Create(domain.Elements()[e], domain);
|
||||
if (!element.HasValue()) return element.GetStatus();
|
||||
view.emplace_back(*element.Value());
|
||||
elements.push_back(std::move(element.Value()));
|
||||
}
|
||||
DofManager dofs;
|
||||
const Status mapping = dofs.Build(model.Value(), view);
|
||||
if (!mapping.IsOk()) return mapping;
|
||||
auto baseline = SolidStaticState::Create(domain, dofs);
|
||||
if (!baseline.HasValue()) return baseline.GetStatus();
|
||||
state = std::move(baseline.Value());
|
||||
std::vector<RuntimeSolid> solids;
|
||||
for (const auto& element : elements) {
|
||||
const auto* hex = dynamic_cast<const Hex8Element*>(element.get());
|
||||
if (hex == nullptr)
|
||||
return Failure(domain, "invalid-solid-static-element",
|
||||
"Solid STATIC requires full-integration Hex8 kernels.",
|
||||
FailureCategory::kModel);
|
||||
auto scatter = dofs.ElementScatter(hex->DofLayout());
|
||||
if (!scatter.HasValue()) return scatter.GetStatus();
|
||||
solids.push_back({hex, std::move(scatter.Value())});
|
||||
}
|
||||
auto stiffness = SparseAssembler::Assemble(view, dofs, parallel_for_);
|
||||
if (!stiffness.HasValue()) return stiffness.GetStatus();
|
||||
const EssentialConstraintPolicy constraints;
|
||||
const auto& step = model.Value().Step();
|
||||
const bool nonlinear = step.IsNonlinearStatic();
|
||||
// No support can remove any rigid mode; fail before a perturbed
|
||||
// factorization.
|
||||
if (dofs.ConstrainedDofCount() == 0U)
|
||||
return Failure(domain, "solid-static-rigid-mechanism",
|
||||
"An unsupported Solid static free system has rigid modes.");
|
||||
if (!nonlinear) {
|
||||
auto partition = constraints.Partition(stiffness.Value(), dofs);
|
||||
if (!partition.HasValue()) return partition.GetStatus();
|
||||
Status status = linear_solver_.Factorize(partition.Value().k_ff);
|
||||
if (!status.IsOk()) return status;
|
||||
// Preserve the required factorization-before-load-assembly lifecycle.
|
||||
auto load = LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs);
|
||||
if (!load.HasValue()) return load.GetStatus();
|
||||
auto rhs = LoadAssembler::EffectiveFreeRhs(
|
||||
load.Value(), partition.Value().k_fc, dofs.PrescribedValues(), dofs);
|
||||
if (!rhs.HasValue()) return rhs.GetStatus();
|
||||
Vector free{dofs.FreeDofCount()};
|
||||
status = linear_solver_.Solve(rhs.Value(), free);
|
||||
if (!status.IsOk()) return status;
|
||||
SolidStaticFrame frame = state.committed_;
|
||||
frame.frame_index = 0U;
|
||||
frame.increment_index = step.IsDirectStatic() ? step.IncrementCount() : 1U;
|
||||
frame.load_factor = 1.0;
|
||||
frame.is_factor_one = true;
|
||||
frame.displacement =
|
||||
constraints.ReconstructFull(free, dofs.PrescribedValues(), dofs);
|
||||
auto recovery =
|
||||
Assemble(solids, dofs, frame.displacement, false, parallel_for_);
|
||||
if (!recovery.HasValue()) return recovery.GetStatus();
|
||||
frame.internal_force = stiffness.Value().Multiply(frame.displacement);
|
||||
frame.external_force = std::move(load.Value());
|
||||
frame.solid_results = std::move(recovery.Value().points);
|
||||
frame.physical_strain_energy = recovery.Value().energy;
|
||||
status = CompleteFrame(domain, dofs, false, stiffness.Value(), frame);
|
||||
return status.IsOk() ? state.CommitAcceptedFrame(frame) : status;
|
||||
}
|
||||
auto factors = Factors(domain);
|
||||
if (!factors.HasValue()) return factors.GetStatus();
|
||||
auto base_load = LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs);
|
||||
if (!base_load.HasValue()) return base_load.GetStatus();
|
||||
for (const auto& increment : factors.Value()) {
|
||||
const Vector start = state.committed_.displacement;
|
||||
Vector trial = start;
|
||||
for (std::size_t c = 0U; c < dofs.ConstrainedDofCount(); ++c)
|
||||
trial[dofs.ConstrainedDofs()[c]] =
|
||||
increment.load_factor * dofs.PrescribedValues()[c];
|
||||
for (std::uint32_t iteration = 1U; iteration <= kMaximumIterations;
|
||||
++iteration) {
|
||||
SolidStaticIterationRecord record;
|
||||
record.increment_index = increment.increment_index;
|
||||
record.load_factor = increment.load_factor;
|
||||
record.iteration = iteration;
|
||||
record.stage = "entered";
|
||||
const auto fail = [&](const Status& failure) {
|
||||
record.diagnostic_code = failure.Diagnostics().empty()
|
||||
? "solid-static-failure"
|
||||
: failure.Diagnostics()[0].code;
|
||||
record.failure_reason = failure.Diagnostics().empty()
|
||||
? "Static trial failed."
|
||||
: failure.Diagnostics()[0].message;
|
||||
state.iterations_.push_back(record);
|
||||
return failure;
|
||||
};
|
||||
record.stage = "trial-recovery";
|
||||
auto assembly = Assemble(solids, dofs, trial, true, parallel_for_);
|
||||
if (!assembly.HasValue()) return fail(assembly.GetStatus());
|
||||
SolidStaticFrame frame = state.committed_;
|
||||
frame.balance = {};
|
||||
frame.global_equilibrium = {};
|
||||
frame.frame_index =
|
||||
static_cast<std::uint32_t>(state.accepted_frames_.size());
|
||||
frame.increment_index = increment.increment_index;
|
||||
frame.load_factor = increment.load_factor;
|
||||
frame.iteration_count = iteration;
|
||||
frame.is_factor_one = increment.is_factor_one;
|
||||
frame.displacement = trial;
|
||||
frame.internal_force = std::move(assembly.Value().force);
|
||||
frame.external_force = base_load.Value();
|
||||
frame.external_force.Scale(increment.load_factor);
|
||||
frame.solid_results = std::move(assembly.Value().points);
|
||||
frame.physical_strain_energy = assembly.Value().energy;
|
||||
auto status = CompleteFrame(domain, dofs, true, stiffness.Value(), frame);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
record.stage = "tangent-partition";
|
||||
auto partition = constraints.Partition(assembly.Value().tangent, dofs);
|
||||
if (!partition.HasValue()) return fail(partition.GetStatus());
|
||||
Vector correction{dofs.FreeDofCount()};
|
||||
// Empty free is vacuous; prescribed geometry/recovery above still runs.
|
||||
if (dofs.FreeDofCount() != 0U) {
|
||||
record.stage = "tangent-factorization";
|
||||
status = tangent_solver_.FactorizeFresh(partition.Value().k_ff);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
auto rhs = constraints.GatherFree(frame.residual, dofs);
|
||||
rhs.Scale(-1.0);
|
||||
record.stage = "correction-solve";
|
||||
status = tangent_solver_.SolveCorrection(rhs, correction);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
}
|
||||
Vector current_increment{dofs.FreeDofCount()};
|
||||
for (std::size_t f = 0U; f < dofs.FreeDofCount(); ++f) {
|
||||
const auto full = dofs.FreeDofs()[f];
|
||||
current_increment[f] = trial[full] - start[full];
|
||||
}
|
||||
record.stage = "convergence";
|
||||
auto convergence = EvaluateSolidStaticConvergence(
|
||||
constraints.GatherFree(frame.residual, dofs),
|
||||
constraints.GatherFree(frame.internal_force, dofs),
|
||||
constraints.GatherFree(frame.external_force, dofs), correction,
|
||||
current_increment);
|
||||
if (!convergence.HasValue()) return fail(convergence.GetStatus());
|
||||
record.force_residual = convergence.Value().force_residual;
|
||||
record.translation_correction =
|
||||
convergence.Value().translation_correction;
|
||||
record.convergence_evaluated = true;
|
||||
if (record.force_residual.passed &&
|
||||
record.translation_correction.passed) {
|
||||
record.stage = "commit";
|
||||
status = state.CommitAcceptedFrame(frame);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
record.accepted = true;
|
||||
state.iterations_.push_back(record);
|
||||
break;
|
||||
}
|
||||
if (iteration == kMaximumIterations)
|
||||
return fail(Failure(domain, "solid-static-nonconvergence",
|
||||
"Solid STATIC exceeded 100 Newton iterations."));
|
||||
for (std::size_t f = 0U; f < dofs.FreeDofCount(); ++f)
|
||||
trial[dofs.FreeDofs()[f]] += correction[f];
|
||||
record.stage = "continue";
|
||||
state.iterations_.push_back(record);
|
||||
}
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
Result<SolidStaticExecutionResult> SolidStaticProcedureExecutor::Execute(
|
||||
const Domain& domain) const {
|
||||
SolidStaticState state;
|
||||
const Status status = ExecuteInto(domain, state);
|
||||
if (!status.IsOk())
|
||||
return Result<SolidStaticExecutionResult>::Failure(status);
|
||||
auto diagnostics = domain.Warnings();
|
||||
SortDiagnostics(diagnostics);
|
||||
return Result<SolidStaticExecutionResult>::Success(
|
||||
{std::move(state), std::move(diagnostics)});
|
||||
}
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "fesa/analysis/solid_static_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/hex8_element.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
/// @brief Keeps state/control failures separate from input syntax failures.
|
||||
Status StateFailure(const char* code, const char* message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kSolver,
|
||||
{{Severity::kError, code, {}, "SOLID_STATIC", {}, message}});
|
||||
}
|
||||
/// @brief Validates all components without a zero clamp or scale floor.
|
||||
template <class Range>
|
||||
bool Finite(const Range& values) {
|
||||
for (double value : values)
|
||||
if (!std::isfinite(value)) return false;
|
||||
return true;
|
||||
}
|
||||
/// @brief Validates owned vectors including their physical full-space size.
|
||||
bool FiniteVector(const Vector& values, std::size_t size) {
|
||||
if (values.Size() != size) return false;
|
||||
for (std::size_t i = 0U; i < size; ++i)
|
||||
if (!std::isfinite(values[i])) return false;
|
||||
return true;
|
||||
}
|
||||
/// @brief Calculates the infinity norm after separate finite validation.
|
||||
double InfinityNorm(const Vector& values) {
|
||||
double norm = 0.0;
|
||||
for (std::size_t i = 0U; i < values.Size(); ++i)
|
||||
norm = (std::max)(norm, std::abs(values[i]));
|
||||
return norm;
|
||||
}
|
||||
/// @brief Compares immutable semantic and raw source identities.
|
||||
bool SameIdentity(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;
|
||||
}
|
||||
/// @brief Builds one raw criterion with the approved branch and physical unit.
|
||||
NewtonChannelDecision Decision(const char* name, double numerator,
|
||||
double denominator, NewtonChannelUnit unit) {
|
||||
const bool absolute = denominator == 0.0;
|
||||
const double threshold =
|
||||
absolute ? kNewtonZeroScaleThreshold : kNewtonRelativeThreshold;
|
||||
return {
|
||||
name,
|
||||
numerator,
|
||||
denominator,
|
||||
absolute ? NewtonConvergenceBranch::kZeroScale
|
||||
: NewtonConvergenceBranch::kRelative,
|
||||
threshold,
|
||||
absolute ? unit : NewtonChannelUnit::kDimensionless,
|
||||
absolute ? numerator <= threshold : numerator / denominator <= threshold};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Result<SolidStaticIterationRecord> EvaluateSolidStaticConvergence(
|
||||
const Vector& residual, const Vector& internal, const Vector& external,
|
||||
const Vector& correction, const Vector& increment) {
|
||||
const std::size_t size = residual.Size();
|
||||
if (!FiniteVector(residual, size) || !FiniteVector(internal, size) ||
|
||||
!FiniteVector(external, size) || !FiniteVector(correction, size) ||
|
||||
!FiniteVector(increment, size))
|
||||
return Result<SolidStaticIterationRecord>::Failure(StateFailure(
|
||||
"nonfinite-solid-newton-data",
|
||||
"Static channels require finite vectors of the same free dimension."));
|
||||
const double external_norm = InfinityNorm(external);
|
||||
const double force_scale =
|
||||
external_norm == 0.0 ? 0.0
|
||||
: (std::max)(external_norm, InfinityNorm(internal));
|
||||
SolidStaticIterationRecord record;
|
||||
record.force_residual = Decision("residual_force", InfinityNorm(residual),
|
||||
force_scale, NewtonChannelUnit::kForce);
|
||||
record.translation_correction =
|
||||
Decision("correction_translation", InfinityNorm(correction),
|
||||
InfinityNorm(increment), NewtonChannelUnit::kLength);
|
||||
record.convergence_evaluated = true;
|
||||
return Result<SolidStaticIterationRecord>::Success(std::move(record));
|
||||
}
|
||||
|
||||
Result<SolidStaticState> SolidStaticState::Create(const Domain& domain,
|
||||
const DofManager& dofs) {
|
||||
if (!domain.IsSolid() || domain.Steps().Size() != 1U ||
|
||||
domain.Frequency() != nullptr || domain.Dynamic() != nullptr ||
|
||||
dofs.Profile() != DofProfile::kSolidStructural ||
|
||||
dofs.ComponentsPerNode() != 3U ||
|
||||
dofs.FullDofCount() != 3U * domain.Nodes().size())
|
||||
return Result<SolidStaticState>::Failure(
|
||||
StateFailure("invalid-solid-static-state",
|
||||
"Static state requires one Solid static step and matching "
|
||||
"3N mapping."));
|
||||
const Status mapping = dofs.ValidateInvariants();
|
||||
if (!mapping.IsOk()) return Result<SolidStaticState>::Failure(mapping);
|
||||
SolidStaticState state;
|
||||
state.initialized_ = true;
|
||||
state.nonlinear_ = domain.Steps()[0].IsNonlinearStatic();
|
||||
state.constrained_dofs_ = dofs.ConstrainedDofs();
|
||||
state.prescribed_values_ = dofs.PrescribedValues();
|
||||
const std::size_t count = dofs.FullDofCount();
|
||||
state.committed_.displacement = Vector{count};
|
||||
state.committed_.internal_force = Vector{count};
|
||||
state.committed_.external_force = Vector{count};
|
||||
state.committed_.residual = Vector{count};
|
||||
state.committed_.reaction = Vector{count};
|
||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||
state.committed_.nodal_internal_order.push_back(
|
||||
static_cast<EntityIndex>(node));
|
||||
state.committed_.nodal_source_ids.push_back(domain.Nodes()[node].source_id);
|
||||
}
|
||||
// Capture the immutable point coordinates from certified zero-state kernels.
|
||||
// Later state commits cannot substitute another element or result location.
|
||||
for (std::size_t e = 0U; e < domain.Elements().Size(); ++e) {
|
||||
auto element = ElementFactory{}.Create(domain.Elements()[e], domain);
|
||||
if (!element.HasValue())
|
||||
return Result<SolidStaticState>::Failure(element.GetStatus());
|
||||
const auto* hex = dynamic_cast<const Hex8Element*>(element.Value().get());
|
||||
if (hex == nullptr)
|
||||
return Result<SolidStaticState>::Failure(StateFailure(
|
||||
"invalid-solid-static-state", "Static state requires Hex8 kernels."));
|
||||
auto response = hex->EvaluateLinear(Vector{24U});
|
||||
if (!response.HasValue())
|
||||
return Result<SolidStaticState>::Failure(response.GetStatus());
|
||||
state.reference_points_.insert(state.reference_points_.end(),
|
||||
response.Value().points.begin(),
|
||||
response.Value().points.end());
|
||||
}
|
||||
return Result<SolidStaticState>::Success(std::move(state));
|
||||
}
|
||||
|
||||
Status SolidStaticState::CommitAcceptedFrame(const SolidStaticFrame& frame) {
|
||||
const auto invalid = []() {
|
||||
return StateFailure(
|
||||
"invalid-solid-static-frame",
|
||||
"Static candidate identity, projection or recovery is invalid.");
|
||||
};
|
||||
if (!initialized_ || frame.frame_index != accepted_frames_.size() ||
|
||||
frame.increment_index == 0U || !std::isfinite(frame.load_factor) ||
|
||||
!(frame.load_factor > committed_.load_factor) ||
|
||||
frame.load_factor > 1.0 ||
|
||||
frame.is_factor_one != (frame.load_factor == 1.0) ||
|
||||
(nonlinear_ &&
|
||||
(frame.increment_index != committed_.increment_index + 1U ||
|
||||
frame.iteration_count == 0U || frame.iteration_count > 100U)) ||
|
||||
(!nonlinear_ && (!accepted_frames_.empty() || frame.load_factor != 1.0 ||
|
||||
frame.iteration_count != 0U)))
|
||||
return invalid();
|
||||
const std::size_t count = committed_.displacement.Size();
|
||||
if (frame.nodal_internal_order != committed_.nodal_internal_order ||
|
||||
frame.nodal_source_ids.size() != committed_.nodal_source_ids.size() ||
|
||||
!FiniteVector(frame.displacement, count) ||
|
||||
!FiniteVector(frame.internal_force, count) ||
|
||||
!FiniteVector(frame.external_force, count) ||
|
||||
!FiniteVector(frame.residual, count) ||
|
||||
!FiniteVector(frame.reaction, count) ||
|
||||
frame.solid_results.size() != reference_points_.size() ||
|
||||
!std::isfinite(frame.physical_strain_energy) ||
|
||||
!Finite(frame.global_equilibrium) ||
|
||||
!Finite(frame.balance.internal_force_sum) ||
|
||||
!Finite(frame.balance.reference_internal_moment) ||
|
||||
!Finite(frame.balance.current_internal_moment) ||
|
||||
!Finite(frame.balance.free_residual_moment) ||
|
||||
!Finite(frame.balance.linearization_moment) ||
|
||||
!Finite(frame.balance.internal_force_scale) ||
|
||||
!Finite(frame.balance.reference_moment_scale) ||
|
||||
!Finite(frame.balance.current_moment_scale))
|
||||
return invalid();
|
||||
for (std::size_t node = 0U; node < frame.nodal_source_ids.size(); ++node)
|
||||
if (!SameIdentity(frame.nodal_source_ids[node],
|
||||
committed_.nodal_source_ids[node]))
|
||||
return invalid();
|
||||
std::vector<bool> constrained(count, false);
|
||||
for (std::size_t c = 0U; c < constrained_dofs_.size(); ++c) {
|
||||
const auto full = constrained_dofs_[c];
|
||||
constrained[full] = true;
|
||||
if (frame.displacement[full] != frame.load_factor * prescribed_values_[c])
|
||||
return invalid();
|
||||
}
|
||||
for (std::size_t i = 0U; i < count; ++i)
|
||||
if (frame.residual[i] !=
|
||||
frame.internal_force[i] - frame.external_force[i] ||
|
||||
frame.reaction[i] != (constrained[i] ? frame.residual[i] : 0.0))
|
||||
return invalid();
|
||||
for (std::size_t g = 0U; g < reference_points_.size(); ++g) {
|
||||
const auto& row = frame.solid_results[g];
|
||||
const auto& reference = reference_points_[g];
|
||||
if (row.element != reference.element ||
|
||||
row.integration_point != reference.integration_point ||
|
||||
row.natural_coordinates != reference.natural_coordinates ||
|
||||
row.reference_coordinates != reference.reference_coordinates ||
|
||||
!Finite(row.cauchy_stress) || !Finite(row.strain))
|
||||
return invalid();
|
||||
}
|
||||
// Prepare both copies before mutating committed history.
|
||||
SolidStaticFrame next = frame;
|
||||
accepted_frames_.push_back(frame);
|
||||
committed_ = std::move(next);
|
||||
return Status::Ok();
|
||||
}
|
||||
const std::vector<SolidStaticFrame>& SolidStaticState::AcceptedFrames()
|
||||
const noexcept {
|
||||
return accepted_frames_;
|
||||
}
|
||||
const SolidStaticFrame& SolidStaticState::CommittedState() const noexcept {
|
||||
return committed_;
|
||||
}
|
||||
const std::vector<SolidStaticIterationRecord>&
|
||||
SolidStaticState::IterationRecords() const noexcept {
|
||||
return iterations_;
|
||||
}
|
||||
} // namespace fesa
|
||||
Reference in New Issue
Block a user