#include "fesa/analysis/solid_static_procedure.h" #include #include #include #include #include #include #include #include #include #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::epsilon(); struct RuntimeSolid { const Hex8Element* element; std::vector scatter; }; struct SolidAssembly { Vector force; SparseMatrix tangent; std::vector 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 Cross(const std::array& a, const std::array& b, std::array* scale = nullptr) { std::array 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> 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(count) != step.TimePeriod()) return Result>::Failure(Failure( domain, "invalid-solid-static-schedule", "DIRECT static requires a finite positive integer fixed schedule.", FailureCategory::kInput)); std::vector 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(n) * step.InitialIncrement() / step.TimePeriod(); if (!std::isfinite(factor) || !(factor > previous) || factor > 1.0) return Result>::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>::Success(std::move(factors)); } /// @brief Builds index-owned trials, then reduces all contributions stably. Result Assemble(const std::vector& solids, const DofManager& dofs, const Vector& displacement, bool nonlinear, const ParallelFor& parallel) { std::vector>> 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 contributions; contributions.reserve(solids.size() * 24U * 24U); std::vector 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::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::Failure(tangent.GetStatus()); return Result::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 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 force_scale{}; std::array reference_moment_scale{}; std::array current_moment_scale{}; std::array identity_moment_scale{}; std::array free_force_sum{}; std::array 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 current{}; std::array u{}; std::array f{}; std::array e{}; std::array 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 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> 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 solids; for (const auto& element : elements) { const auto* hex = dynamic_cast(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 = std::move(recovery.Value().force); 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(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 SolidStaticProcedureExecutor::Execute( const Domain& domain) const { SolidStaticState state; const Status status = ExecuteInto(domain, state); if (!status.IsOk()) return Result::Failure(status); auto diagnostics = domain.Warnings(); SortDiagnostics(diagnostics); return Result::Success( {std::move(state), std::move(diagnostics)}); } } // namespace fesa