feat: adopt lumped mass and reduced-coordinate shell HHT

Integrate Hex8 mass verification and fixed-null-basis shell dynamics with numerical and physics handoff evidence. Preserve interrupted reference and full-validation status; full integration validation is deferred until after performance optimization.
This commit is contained in:
KOKO\Mimi
2026-10-13 16:57:19 +09:00
parent ede137c6b4
commit eaa5cbeb21
110 changed files with 15274 additions and 971 deletions
+3 -307
View File
@@ -1,118 +1,8 @@
#include "fesa/analysis/hht_dynamic_procedure.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <string>
#include <utility>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/solvers/linear/tangent_solver.h"
#include "analysis/hht_reduced_procedure.h"
namespace fesa {
namespace {
using namespace hht_internal;
/// @brief Creates one procedure-owned diagnostic with the source step.
Status Failure(const Domain& domain, const std::string& code,
const std::string& message) {
return Status::Failure(
FailureCategory::kSolver,
{{Severity::kError, code, domain.Dynamic()->dynamic_location, "DYNAMIC",
domain.Dynamic()->source_step_name_raw, message}});
}
/// @brief Checks one update against its uncancelled, dimensionally equal terms.
bool UpdateEqual(const double left, const std::initializer_list<double> terms) {
double right = 0.0;
double scale = 0.0;
for (const double term : terms) {
right += term;
scale += std::abs(term);
}
const double defect = std::abs(left - right);
return std::isfinite(defect) && std::isfinite(scale) &&
(scale == 0.0 ? defect == 0.0 : defect / scale <= 1e-12);
}
/// @brief Verifies both absolute and increment forms before accepting history.
Status CheckUpdates(const Domain& domain, const DynamicAcceptedFrameState& old,
const DynamicAcceptedFrameState& frame,
const HhtPredictor& predictor, const HhtParameters& p,
const double h) {
for (std::size_t i = 0U; i < frame.generalized_coordinates.Size(); ++i) {
const double increment =
frame.generalized_coordinates[i] - old.generalized_coordinates[i];
const double old_term =
h * h * (0.5 - p.beta) * old.algorithmic_acceleration[i];
const double new_term = h * h * p.beta * frame.algorithmic_acceleration[i];
if (!UpdateEqual(frame.generalized_coordinates[i],
{old.generalized_coordinates[i], h * old.velocity[i],
old_term, new_term}) ||
!UpdateEqual(increment, {h * old.velocity[i], old_term, new_term}) ||
!UpdateEqual(
frame.generalized_coordinates[i] - predictor.coordinates[i],
{new_term}) ||
!UpdateEqual(frame.velocity[i],
{old.velocity[i],
h * (1 - p.gamma) * old.algorithmic_acceleration[i],
h * p.gamma * frame.algorithmic_acceleration[i]}) ||
!UpdateEqual(frame.algorithmic_acceleration[i],
{(1 + p.alpha) * frame.acceleration[i],
-p.alpha * old.acceleration[i]})) {
return Failure(
domain, "dynamic-update-inconsistency",
"HHT endpoint update or resolved increment exceeds 1e-12.");
}
}
return Status::Ok();
}
/// @brief Fills the raw channel evidence without hiding a zero denominator.
void ResidualRecord(const DofManager& dofs,
const DynamicAcceptedFrameState& frame,
DynamicNewtonIterationLogRecord* record) {
record->residual_numerator = {};
record->residual_scale = {};
for (const auto full : dofs.FreeDofs()) {
const auto c = full % 6U < 3U ? 0U : 1U;
record->residual_numerator[c] = (std::max)(record->residual_numerator[c],
std::abs(frame.residual[full]));
record->residual_scale[c] =
(std::max)(record->residual_scale[c], frame.residual_scale[full]);
}
record->residual_pass = frame.free_residual_metrics[0U] <= 1e-10 &&
frame.free_residual_metrics[1U] <= 1e-10;
}
/// @brief Forms the four coordinate/rate correction checks in scaled units.
void CorrectionRecord(const Vector& correction, const DofManager& dofs,
const double length, const HhtParameters& p,
DynamicNewtonIterationLogRecord* record) {
record->coordinate_correction = {};
for (const auto full : dofs.FreeDofs()) {
const auto c = full % 6U < 3U ? 0U : 1U;
record->coordinate_correction[c] =
(std::max)(record->coordinate_correction[c],
std::abs(correction[full]) / (c == 0U ? length : 1.0));
}
record->correction_pass = true;
for (std::size_t c = 0; c < 2U; ++c) {
const double dq = record->coordinate_correction[c];
record->velocity_correction[c] = p.gamma / p.beta * dq;
record->acceleration_correction[c] = dq / ((1 + p.alpha) * p.beta);
record->algorithmic_correction[c] = dq / p.beta;
for (const auto* channel :
{&record->coordinate_correction, &record->velocity_correction,
&record->acceleration_correction, &record->algorithmic_correction})
record->correction_pass =
record->correction_pass && (*channel)[c] <= 1e-10;
}
}
} // namespace
HhtDynamicProcedureExecutor::HhtDynamicProcedureExecutor(
const ParallelFor& parallel_for, LinearSolver& positive_solver,
TangentSolver& tangent_solver)
@@ -122,201 +12,7 @@ HhtDynamicProcedureExecutor::HhtDynamicProcedureExecutor(
Result<DynamicExecutionResult> HhtDynamicProcedureExecutor::Execute(
const Domain& domain, NewtonIterationLogWriter& log) const {
if (domain.Dynamic() == nullptr) {
return Result<DynamicExecutionResult>::Failure(Status::Failure(
FailureCategory::kModel, {{Severity::kError,
"invalid-dynamic-step",
{},
"DYNAMIC",
{},
"HHT requires exactly one dynamic step."}}));
}
auto model = AnalysisModel::Create(domain);
if (!model.HasValue())
return Result<DynamicExecutionResult>::Failure(model.GetStatus());
auto dofs_result = DofManager::Create(model.Value());
if (!dofs_result.HasValue())
return Result<DynamicExecutionResult>::Failure(dofs_result.GetStatus());
const auto& dofs = dofs_result.Value();
auto state_result = DynamicAnalysisState::Create(domain, dofs);
if (!state_result.HasValue())
return Result<DynamicExecutionResult>::Failure(state_result.GetStatus());
auto state = std::move(state_result.Value());
auto shells = BuildRuntimeShells(domain, dofs);
if (!shells.HasValue())
return Result<DynamicExecutionResult>::Failure(shells.GetStatus());
auto length_result = ReferenceLength(domain);
if (!length_result.HasValue())
return Result<DynamicExecutionResult>::Failure(length_result.GetStatus());
const double length = length_result.Value();
auto loads = AssembleLoads(domain, dofs);
if (!loads.HasValue())
return Result<DynamicExecutionResult>::Failure(loads.GetStatus());
auto parameters = MakeHhtParameters(domain.Dynamic()->alpha);
if (!parameters.HasValue())
return Result<DynamicExecutionResult>::Failure(parameters.GetStatus());
const auto p = parameters.Value();
const double h = domain.Dynamic()->initial_increment;
DynamicAcceptedFrameState frame{};
frame.is_initial = true;
for (auto* vector : {&frame.generalized_coordinates, &frame.velocity,
&frame.acceleration, &frame.algorithmic_acceleration})
*vector = Vector(dofs.FullDofCount());
frame.external_force = loads.Value()[0U];
auto initial = EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!initial.HasValue())
return Result<DynamicExecutionResult>::Failure(initial.GetStatus());
const auto null_nodes = NullNodes(dofs, initial.Value().rotations);
auto acceleration = InitializeAcceleration(
domain, dofs, initial.Value(), length, positive_solver_, tangent_solver_);
if (!acceleration.HasValue())
return Result<DynamicExecutionResult>::Failure(acceleration.GetStatus());
frame.acceleration = acceleration.Value();
frame.algorithmic_acceleration = acceleration.Value();
initial = EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!initial.HasValue())
return Result<DynamicExecutionResult>::Failure(initial.GetStatus());
frame = initial.Value().frame;
Status status = CompleteConstraintEvidence(
domain, dofs, initial.Value(), length, null_nodes, positive_solver_,
tangent_solver_, &frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
status = CompleteEquilibrium(domain, dofs, initial.Value(), &frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
const double initial_energy = frame.kinetic_energy +
frame.physical_strain_energy +
frame.drilling_energy;
status = state.CommitAcceptedFrame(frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
DynamicNewtonIterationLogRecord record{};
record.stage = "accepted";
record.outcome = "initial";
record.correction_pass = true;
ResidualRecord(dofs, frame, &record);
status = log.Write(record);
if (status.IsOk()) status = log.Flush();
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
for (std::uint32_t increment = 1U;
increment <= domain.Dynamic()->increment_count; ++increment) {
const auto& old = state.CommittedState();
auto predictor = PredictHht(old.generalized_coordinates, old.velocity,
old.algorithmic_acceleration, h, p);
if (!predictor.HasValue())
return Result<DynamicExecutionResult>::Failure(predictor.GetStatus());
frame = old;
frame.is_initial = false;
frame.frame_index = increment;
frame.increment_index = increment;
frame.time = static_cast<double>(increment) * h;
// A large DIRECT step may put a kinematic predictor outside the shell
// chart before Newton starts. The last accepted geometry is a valid
// initial iterate; q_p still defines the unchanged HHT rate equations.
frame.generalized_coordinates = old.generalized_coordinates;
record = {};
record.increment_index = increment;
record.time = frame.time;
record.correction_pass = true;
bool accepted = false;
for (std::uint32_t iteration = 0U; iteration <= 100U; ++iteration) {
record.iteration_index = iteration;
record.stage = "evaluation";
record.outcome = "candidate";
auto fail = [&](const Status& failure) -> Result<DynamicExecutionResult> {
record.stage = "failed";
record.outcome = "failed";
record.failure_code = failure.Diagnostics().empty()
? "dynamic-solver-failure"
: failure.Diagnostics().front().code;
const Status written = log.Write(record);
return Result<DynamicExecutionResult>::Failure(
written.IsOk() ? failure : written);
};
auto rates = EvaluateHhtRates(frame.generalized_coordinates,
predictor.Value(), old.acceleration, h, p);
if (!rates.HasValue()) return fail(rates.GetStatus());
frame.velocity = std::move(rates.Value().velocity);
frame.acceleration = std::move(rates.Value().acceleration);
frame.algorithmic_acceleration =
std::move(rates.Value().algorithmic_acceleration);
frame.iteration_count = iteration;
auto assembly =
EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, false);
if (!assembly.HasValue()) return fail(assembly.GetStatus());
frame = assembly.Value().frame;
ResidualRecord(dofs, frame, &record);
if (record.residual_pass && record.correction_pass) {
assembly =
EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!assembly.HasValue()) return fail(assembly.GetStatus());
frame = assembly.Value().frame;
status = CompleteConstraintEvidence(
domain, dofs, assembly.Value(), length, null_nodes,
positive_solver_, tangent_solver_, &frame);
if (!status.IsOk()) return fail(status);
status = CompleteEquilibrium(domain, dofs, assembly.Value(), &frame);
if (!status.IsOk()) return fail(status);
status = CheckUpdates(domain, old, frame, predictor.Value(), p, h);
if (!status.IsOk()) return fail(status);
frame.external_work = 0.0;
for (std::size_t i = 0; i < dofs.FullDofCount(); ++i)
frame.external_work +=
frame.external_force[i] * frame.generalized_coordinates[i];
frame.energy_defect =
frame.kinetic_energy + frame.physical_strain_energy +
frame.drilling_energy - initial_energy - frame.external_work;
status = state.CommitAcceptedFrame(frame);
if (!status.IsOk()) return fail(status);
record.stage = "accepted";
record.outcome = "accepted";
status = log.Write(record);
if (status.IsOk()) status = log.Flush();
if (!status.IsOk())
return Result<DynamicExecutionResult>::Failure(status);
accepted = true;
break;
}
if (iteration == 100U)
return fail(Failure(domain, "dynamic-nonconvergence",
"HHT exhausted 100 corrections without satisfying "
"every endpoint check."));
status = log.Write(record);
if (!status.IsOk())
return Result<DynamicExecutionResult>::Failure(status);
auto tangent =
ScaledFree(assembly.Value().effective_tangent, dofs, length);
if (!tangent.HasValue()) return fail(tangent.GetStatus());
Vector rhs(dofs.FreeDofCount());
Vector solution(dofs.FreeDofCount());
Vector correction(dofs.FullDofCount());
for (std::size_t i = 0; i < dofs.FreeDofCount(); ++i) {
const auto full = dofs.FreeDofs()[i];
rhs[i] = -frame.residual[full] * (full % 6U < 3U ? length : 1.0);
}
if (dofs.FreeDofCount() > 0U) {
status = tangent_solver_.FactorizeFresh(tangent.Value());
if (!status.IsOk()) return fail(status);
status = tangent_solver_.SolveCorrection(rhs, solution);
if (!status.IsOk()) return fail(status);
status = CheckLinearSolve(tangent.Value(), rhs, solution);
if (!status.IsOk()) return fail(status);
}
for (std::size_t i = 0; i < dofs.FreeDofCount(); ++i) {
const auto full = dofs.FreeDofs()[i];
correction[full] = solution[i] * (full % 6U < 3U ? length : 1.0);
frame.generalized_coordinates[full] += correction[full];
}
CorrectionRecord(correction, dofs, length, p, &record);
}
if (!accepted)
return Result<DynamicExecutionResult>::Failure(
Failure(domain, "dynamic-nonconvergence",
"Dynamic increment was not accepted."));
}
return Result<DynamicExecutionResult>::Success(
{std::move(state), domain.Warnings()});
return ExecuteReducedHht(domain, log, parallel_for_, positive_solver_,
tangent_solver_);
}
} // namespace fesa