#include "fesa/analysis/hht_dynamic_procedure.h" #include #include #include #include #include #include "analysis/hht_dynamic_internal.h" #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/newton_iteration_log.h" #include "fesa/solvers/linear/tangent_solver.h" namespace fesa { namespace { using namespace hht_internal; /// @brief Creates one procedure-owned diagnostic with the source step. Status Failure(const Domain& domain, const std::string& code, const std::string& message) { return Status::Failure( FailureCategory::kSolver, {{Severity::kError, code, domain.Dynamic()->dynamic_location, "DYNAMIC", domain.Dynamic()->source_step_name_raw, message}}); } /// @brief Checks one update against its uncancelled, dimensionally equal terms. bool UpdateEqual(const double left, const std::initializer_list terms) { double right = 0.0; double scale = 0.0; for (const double term : terms) { right += term; scale += std::abs(term); } const double defect = std::abs(left - right); return std::isfinite(defect) && std::isfinite(scale) && (scale == 0.0 ? defect == 0.0 : defect / scale <= 1e-12); } /// @brief Verifies both absolute and increment forms before accepting history. Status CheckUpdates(const Domain& domain, const DynamicAcceptedFrameState& old, const DynamicAcceptedFrameState& frame, const HhtPredictor& predictor, const HhtParameters& p, const double h) { for (std::size_t i = 0U; i < frame.generalized_coordinates.Size(); ++i) { const double increment = frame.generalized_coordinates[i] - old.generalized_coordinates[i]; const double old_term = h * h * (0.5 - p.beta) * old.algorithmic_acceleration[i]; const double new_term = h * h * p.beta * frame.algorithmic_acceleration[i]; if (!UpdateEqual(frame.generalized_coordinates[i], {old.generalized_coordinates[i], h * old.velocity[i], old_term, new_term}) || !UpdateEqual(increment, {h * old.velocity[i], old_term, new_term}) || !UpdateEqual( frame.generalized_coordinates[i] - predictor.coordinates[i], {new_term}) || !UpdateEqual(frame.velocity[i], {old.velocity[i], h * (1 - p.gamma) * old.algorithmic_acceleration[i], h * p.gamma * frame.algorithmic_acceleration[i]}) || !UpdateEqual(frame.algorithmic_acceleration[i], {(1 + p.alpha) * frame.acceleration[i], -p.alpha * old.acceleration[i]})) { return Failure( domain, "dynamic-update-inconsistency", "HHT endpoint update or resolved increment exceeds 1e-12."); } } return Status::Ok(); } /// @brief Fills the raw channel evidence without hiding a zero denominator. void ResidualRecord(const DofManager& dofs, const DynamicAcceptedFrameState& frame, DynamicNewtonIterationLogRecord* record) { record->residual_numerator = {}; record->residual_scale = {}; for (const auto full : dofs.FreeDofs()) { const auto c = full % 6U < 3U ? 0U : 1U; record->residual_numerator[c] = (std::max)(record->residual_numerator[c], std::abs(frame.residual[full])); record->residual_scale[c] = (std::max)(record->residual_scale[c], frame.residual_scale[full]); } record->residual_pass = frame.free_residual_metrics[0U] <= 1e-10 && frame.free_residual_metrics[1U] <= 1e-10; } /// @brief Forms the four coordinate/rate correction checks in scaled units. void CorrectionRecord(const Vector& correction, const DofManager& dofs, const double length, const HhtParameters& p, DynamicNewtonIterationLogRecord* record) { record->coordinate_correction = {}; for (const auto full : dofs.FreeDofs()) { const auto c = full % 6U < 3U ? 0U : 1U; record->coordinate_correction[c] = (std::max)(record->coordinate_correction[c], std::abs(correction[full]) / (c == 0U ? length : 1.0)); } record->correction_pass = true; for (std::size_t c = 0; c < 2U; ++c) { const double dq = record->coordinate_correction[c]; record->velocity_correction[c] = p.gamma / p.beta * dq; record->acceleration_correction[c] = dq / ((1 + p.alpha) * p.beta); record->algorithmic_correction[c] = dq / p.beta; for (const auto* channel : {&record->coordinate_correction, &record->velocity_correction, &record->acceleration_correction, &record->algorithmic_correction}) record->correction_pass = record->correction_pass && (*channel)[c] <= 1e-10; } } } // namespace HhtDynamicProcedureExecutor::HhtDynamicProcedureExecutor( const ParallelFor& parallel_for, LinearSolver& positive_solver, TangentSolver& tangent_solver) : parallel_for_(parallel_for), positive_solver_(positive_solver), tangent_solver_(tangent_solver) {} Result HhtDynamicProcedureExecutor::Execute( const Domain& domain, NewtonIterationLogWriter& log) const { if (domain.Dynamic() == nullptr) { return Result::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::Failure(model.GetStatus()); auto dofs_result = DofManager::Create(model.Value()); if (!dofs_result.HasValue()) return Result::Failure(dofs_result.GetStatus()); const auto& dofs = dofs_result.Value(); auto state_result = DynamicAnalysisState::Create(domain, dofs); if (!state_result.HasValue()) return Result::Failure(state_result.GetStatus()); auto state = std::move(state_result.Value()); auto shells = BuildRuntimeShells(domain, dofs); if (!shells.HasValue()) return Result::Failure(shells.GetStatus()); auto length_result = ReferenceLength(domain); if (!length_result.HasValue()) return Result::Failure(length_result.GetStatus()); const double length = length_result.Value(); auto loads = AssembleLoads(domain, dofs); if (!loads.HasValue()) return Result::Failure(loads.GetStatus()); auto parameters = MakeHhtParameters(domain.Dynamic()->alpha); if (!parameters.HasValue()) return Result::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::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::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::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::Failure(status); status = CompleteEquilibrium(domain, dofs, initial.Value(), &frame); if (!status.IsOk()) return Result::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::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::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::Failure(predictor.GetStatus()); frame = old; frame.is_initial = false; frame.frame_index = increment; frame.increment_index = increment; frame.time = static_cast(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 { 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::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::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::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::Failure( Failure(domain, "dynamic-nonconvergence", "Dynamic increment was not accepted.")); } return Result::Success( {std::move(state), domain.Warnings()}); } } // namespace fesa