#include #include #include #include #include #include #include #include "analysis/hht_dynamic_internal.h" #include "fesa/assembly/parallel_for.h" #include "fesa/elements/finite_rotation.h" #include "fesa/loads/load.h" #include "fesa/model/source_target_resolver.h" namespace fesa::hht_internal { namespace { /// @brief Reports a dynamic assembly or equilibrium failure. Status Failure(const std::string& message) { return Status::Failure(FailureCategory::kSolver, {{Severity::kError, "dynamic-invalid-state", {}, "DYNAMIC", {}, message}}); } /// @brief Uses exact zero semantics for a dimensionally consistent ratio. double Ratio(const double numerator, const double denominator) { return denominator > 0.0 ? numerator / denominator : numerator == 0.0 ? 0.0 : (std::numeric_limits::infinity)(); } /// @brief Gathers one source-node Cartesian component triple. Vector3 Triple(const Vector& vector, const std::size_t first) { return {vector[first], vector[first + 1U], vector[first + 2U]}; } /// @brief Computes one matrix-vector spatial transform. Vector3 Transform(const Matrix& matrix, const Vector3& vector) { return {matrix(0U, 0U) * vector[0U] + matrix(0U, 1U) * vector[1U] + matrix(0U, 2U) * vector[2U], matrix(1U, 0U) * vector[0U] + matrix(1U, 1U) * vector[1U] + matrix(1U, 2U) * vector[2U], matrix(2U, 0U) * vector[0U] + matrix(2U, 1U) * vector[1U] + matrix(2U, 2U) * vector[2U]}; } /// @brief Gathers a local 24-vector in the DOF-owner scatter order. Vector Gather(const Vector& full, const RuntimeShell& shell) { Vector local(24U); for (std::size_t i = 0U; i < 24U; ++i) local[i] = full[shell.scatter[i]]; return local; } } // namespace Result> BuildRuntimeShells(const Domain& domain, const DofManager& dofs) { std::vector shells; for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { const auto* definition = dynamic_cast(&domain.Elements()[index]); if (definition == nullptr || definition->section_index >= domain.Properties().Size() || definition->material_index >= domain.Materials().Size()) { return Result>::Failure( Failure("HHT requires valid MITC4 shell definitions.")); } const auto* section = dynamic_cast( &domain.Properties()[definition->section_index]); const auto* material = dynamic_cast( &domain.Materials()[definition->material_index]); if (section == nullptr || material == nullptr) { return Result>::Failure( Failure("HHT requires homogeneous elastic shell properties.")); } double density = 0.0; for (const auto& record : domain.Densities()) { if (record.material_index == definition->material_index) density = record.rho; } std::array nodes{}; std::array frames{}; for (std::size_t local = 0U; local < 4U; ++local) { const auto node = definition->node_indices[local]; if (node >= domain.Nodes().size() || node >= domain.ShellNodeInitialFrames().size()) { return Result>::Failure( Failure("A dynamic shell node has no initial frame.")); } nodes[local] = &domain.Nodes()[node]; frames[local] = domain.ShellNodeInitialFrames()[node]; } auto internal = NonlinearMitc4Shell::Create(nodes, frames, *section, *material); if (!internal.HasValue()) return Result>::Failure(internal.GetStatus()); auto inertia = NonlinearMitc4Inertia::Create(nodes, frames, *section, *material, density); if (!inertia.HasValue()) return Result>::Failure(inertia.GetStatus()); shells.push_back({static_cast(index), dofs.ShellElementScatter(static_cast(index)), std::move(internal.Value()), std::move(inertia.Value())}); } return Result>::Success(std::move(shells)); } Result ReferenceLength(const Domain& domain) { double length = 0.0; for (std::size_t i = 0U; i < domain.ShellElements().Size(); ++i) { const auto& element = domain.ShellElements()[i]; for (std::size_t node = 0U; node < 4U; ++node) { const Vector3 a(domain.Nodes()[element.node_indices[node]].coordinates); const Vector3 b( domain.Nodes()[element.node_indices[(node + 1U) % 4U]].coordinates); length = (std::max)(length, (a - b).Norm()); } } if (!std::isfinite(length) || length <= 0.0) return Result::Failure( Failure("The dynamic reference length must be positive.")); return Result::Success(length); } Result> AssembleLoads(const Domain& domain, const DofManager& dofs) { const auto index = SourceTargetIndex::FromDomain(domain); const SourceTargetResolver resolver(index); const LoadContext context{domain, dofs, resolver}; std::array result{Vector(dofs.FullDofCount()), Vector(dofs.FullDofCount())}; for (const auto& load : domain.Steps()[0U].Loads()) { const auto contributions = load.get().ComputeContributions(context); if (!contributions.HasValue()) return Result>::Failure(contributions.GetStatus()); for (const auto& entry : contributions.Value()) { result[0U][entry.full_dof_index] += entry.value; result[1U][entry.full_dof_index] += std::abs(entry.value); if (!std::isfinite(result[0U][entry.full_dof_index]) || !std::isfinite(result[1U][entry.full_dof_index])) return Result>::Failure( Failure("Dynamic load reduction overflowed.")); } } return Result>::Success(std::move(result)); } Result MakeSparse(const std::size_t rows, const std::size_t columns, std::vector contributions) { std::vector> row_columns(rows); for (const auto& entry : contributions) { if (entry.row >= rows || entry.column >= columns) return Result::Failure( Failure("Projected sparse index is invalid.")); row_columns[entry.row].push_back(entry.column); } SparsePattern pattern; pattern.row_offsets.push_back(0U); for (auto& row : row_columns) { std::sort(row.begin(), row.end()); row.erase(std::unique(row.begin(), row.end()), row.end()); pattern.column_indices.insert(pattern.column_indices.end(), row.begin(), row.end()); pattern.row_offsets.push_back(pattern.column_indices.size()); } return SparseMatrix::FromCoo(rows, columns, std::move(contributions), pattern); } Result ScaledFree(const SparseMatrix& matrix, const DofManager& dofs, const double length) { std::vector entries; for (std::size_t i = 0U; i < dofs.FreeDofCount(); ++i) { const auto row = dofs.FreeDofs()[i]; for (auto offset = matrix.RowOffsets()[row]; offset < matrix.RowOffsets()[row + 1U]; ++offset) { const auto column = matrix.ColumnIndices()[offset]; const auto free_column = dofs.FreeEquation(column); if (free_column.has_value()) { const double factor = (row % 6U < 3U ? length : 1.0) * (column % 6U < 3U ? length : 1.0); entries.push_back( {i, *free_column, factor * matrix.Values()[offset], row, offset}); } } } return MakeSparse(dofs.FreeDofCount(), dofs.FreeDofCount(), std::move(entries)); } Status CheckLinearSolve(const SparseMatrix& matrix, const Vector& rhs, const Vector& solution) { if (matrix.Rows() != rhs.Size() || matrix.Columns() != solution.Size()) return Failure("Dynamic linear solve dimensions differ."); double numerator = 0.0; double denominator = 0.0; for (std::size_t row = 0U; row < matrix.Rows(); ++row) { double residual = -rhs[row]; double scale = std::abs(rhs[row]); for (auto offset = matrix.RowOffsets()[row]; offset < matrix.RowOffsets()[row + 1U]; ++offset) { const double term = matrix.Values()[offset] * solution[matrix.ColumnIndices()[offset]]; residual += term; scale += std::abs(term); } if (!std::isfinite(residual) || !std::isfinite(scale)) return Failure("Dynamic linear solve returned nonfinite data."); numerator = (std::max)(numerator, std::abs(residual)); denominator = (std::max)(denominator, scale); } if (Ratio(numerator, denominator) > 1e-12) { return Status::Failure(FailureCategory::kSolver, {{Severity::kError, "dynamic-linear-solve", {}, "DYNAMIC", {}, "Dynamic linear backward error exceeds 1e-12."}}); } return Status::Ok(); } Result EvaluateAssembly( const Domain& domain, const DofManager& dofs, const std::vector& shells, DynamicAcceptedFrameState frame, const Vector& load_scale, const double time_increment, const HhtParameters& parameters, const ParallelFor& parallel_for, const bool directional_third) { const std::size_t count = dofs.FullDofCount(); for (const auto* values : std::array{ &frame.generalized_coordinates, &frame.velocity, &frame.acceleration, &frame.algorithmic_acceleration, &frame.external_force, &load_scale}) { if (values->Size() != count) return Result::Failure( Failure("Dynamic trial vector size is invalid.")); for (std::size_t i = 0U; i < count; ++i) if (!std::isfinite((*values)[i])) return Result::Failure( Failure("Dynamic trial contains nonfinite values.")); } frame.nodal_internal_order.clear(); frame.nodal_source_ids.clear(); frame.nodal_triad.clear(); frame.nodal_director.clear(); frame.angular_velocity = Vector(3U * domain.Nodes().size()); frame.angular_acceleration = Vector(3U * domain.Nodes().size()); std::vector rotations; for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { const auto phi = Triple(frame.generalized_coordinates, 6U * node + 3U); const auto& baseline = domain.ShellNodeInitialFrames()[node]; auto rotation = EvaluateDynamicRotation( phi, Vector3(baseline.director), Triple(frame.velocity, 6U * node + 3U), Triple(frame.acceleration, 6U * node + 3U)); if (!rotation.HasValue()) return Result::Failure(rotation.GetStatus()); const Matrix r = ExpSkew(phi); frame.nodal_internal_order.push_back(static_cast(node)); frame.nodal_source_ids.push_back(domain.Nodes()[node].source_id); frame.nodal_director.push_back(rotation.Value().director.Components()); frame.nodal_triad.push_back( {Transform(r, Vector3(baseline.tangent_a)).Components(), Transform(r, Vector3(baseline.tangent_b)).Components(), rotation.Value().director.Components()}); for (std::size_t c = 0U; c < 3U; ++c) { frame.angular_velocity[3U * node + c] = rotation.Value().angular_velocity[c]; frame.angular_acceleration[3U * node + c] = rotation.Value().angular_acceleration[c]; } rotations.push_back(std::move(rotation.Value())); } std::vector> internal( shells.size()); std::vector> inertia( shells.size()); std::vector> failures(shells.size()); parallel_for.Execute(shells.size(), [&](const std::size_t e) { const auto q = Gather(frame.generalized_coordinates, shells[e]); const auto v = Gather(frame.velocity, shells[e]); const auto a = Gather(frame.acceleration, shells[e]); NonlinearMitc4TrialDofs trial{}; for (std::size_t node = 0U; node < 4U; ++node) { trial.displacement[node] = Triple(q, 6U * node); trial.rotation_vector[node] = Triple(q, 6U * node + 3U); } auto f = EvaluateInternalResidualAndTangent( shells[e].internal, trial, directional_third ? &v : nullptr); if (!f.HasValue()) { failures[e] = f.GetStatus(); return; } auto m = shells[e].inertia.Evaluate(trial, v, a); if (!m.HasValue()) { failures[e] = m.GetStatus(); return; } internal[e] = std::move(f.Value()); inertia[e] = std::move(m.Value()); }); frame.inertia_force = Vector(count); frame.internal_force = Vector(count); frame.residual = Vector(count); frame.reaction = Vector(count); frame.residual_scale = load_scale; frame.shell_results.clear(); frame.kinetic_energy = frame.physical_strain_energy = frame.drilling_energy = 0.0; frame.linear_momentum = {}; frame.angular_momentum = {}; frame.linear_momentum_rate = {}; frame.angular_momentum_rate = {}; std::vector, double>> constraint_hessians(domain.Nodes().size()); Vector third(count); Vector third_scale(count); std::array, 4> entries; for (auto& values : entries) values.reserve(shells.size() * 24U * 24U); double momentum_scale = 0.0; double angular_scale = 0.0; const double a_coefficient = 1.0 / ((1.0 + parameters.alpha) * parameters.beta * time_increment * time_increment); const double v_coefficient = parameters.gamma / (parameters.beta * time_increment); for (std::size_t e = 0U; e < shells.size(); ++e) { if (failures[e].has_value()) return Result::Failure(*failures[e]); const auto& f = *internal[e]; const auto& m = *inertia[e]; if (directional_third) { for (std::size_t local = 0U; local < 4U; ++local) { auto& hessian = constraint_hessians[shells[e].scatter[6U * local] / 6U]; for (std::size_t i = 0U; i < 24U; ++i) for (std::size_t j = 0U; j < 24U; ++j) { hessian[{shells[e].scatter[i], shells[e].scatter[j]}] += f.drilling_constraint_hessian[local](i, j); } } } const auto a = Gather(frame.acceleration, shells[e]); for (std::size_t row = 0U; row < 24U; ++row) { const auto global = shells[e].scatter[row]; frame.internal_force[global] += f.internal_force[row]; frame.inertia_force[global] += m.force[row]; third[global] += f.force_directional_second[row]; third_scale[global] += std::abs(f.force_directional_second[row]); frame.residual_scale[global] += std::abs(m.convective_force[row]) + std::abs(f.physical_force[row]) + std::abs(f.drilling_force[row]); for (std::size_t column = 0U; column < 24U; ++column) { frame.residual_scale[global] += std::abs(m.mass(row, column) * a[column]); const std::array values{ m.mass(row, column), f.internal_tangent(row, column), f.internal_tangent(row, column) + m.position_tangent(row, column) + v_coefficient * m.velocity_tangent(row, column) + a_coefficient * m.mass(row, column), f.drilling_metric(row, column)}; for (std::size_t k = 0U; k < 4U; ++k) entries[k].push_back({global, shells[e].scatter[column], values[k], e, row * 24U + column}); } } for (const auto& point : f.recovery.gauss_points) { frame.shell_results.push_back( {shells[e].element_index, point.location, point.natural_coordinates, {point.local_frame.e1, point.local_frame.e2, point.local_frame.e3}, point.generalized_strain, point.section_resultant, point.stress}); } frame.kinetic_energy += m.kinetic_energy; frame.physical_strain_energy += f.physical_strain_energy; frame.drilling_energy += f.drilling_energy; momentum_scale += m.momentum_rate_scale; angular_scale += m.angular_momentum_rate_scale; for (std::size_t c = 0U; c < 3U; ++c) { frame.linear_momentum[c] += m.momentum[c]; frame.angular_momentum[c] += m.angular_momentum[c]; frame.linear_momentum_rate[c] += m.momentum_rate[c]; frame.angular_momentum_rate[c] += m.angular_momentum_rate[c]; } } for (std::size_t i = 0U; i < count; ++i) { frame.residual[i] = frame.inertia_force[i] + frame.internal_force[i] - frame.external_force[i]; if (!std::isfinite(frame.residual[i]) || !std::isfinite(frame.residual_scale[i]) || !std::isfinite(third[i]) || !std::isfinite(third_scale[i])) { return Result::Failure( Failure("Dynamic force reduction overflowed.")); } } for (const auto i : dofs.ConstrainedDofs()) frame.reaction[i] = frame.residual[i]; std::array residual_norm{}; std::array scale_norm{}; for (const auto i : dofs.FreeDofs()) { const auto channel = i % 6U < 3U ? 0U : 1U; residual_norm[channel] = (std::max)(residual_norm[channel], std::abs(frame.residual[i])); scale_norm[channel] = (std::max)(scale_norm[channel], frame.residual_scale[i]); } for (std::size_t c = 0U; c < 2U; ++c) frame.free_residual_metrics[c] = Ratio(residual_norm[c], scale_norm[c]); auto mass = SparseMatrix::FromCoo(count, count, std::move(entries[0U]), dofs.GetSparsePattern()); auto stiffness = SparseMatrix::FromCoo(count, count, std::move(entries[1U]), dofs.GetSparsePattern()); auto effective = SparseMatrix::FromCoo(count, count, std::move(entries[2U]), dofs.GetSparsePattern()); auto metric = SparseMatrix::FromCoo(count, count, std::move(entries[3U]), dofs.GetSparsePattern()); for (const auto* result : {&mass, &stiffness, &effective, &metric}) if (!result->HasValue()) return Result::Failure(result->GetStatus()); Vector hidden_second(domain.Nodes().size()); Vector hidden_second_scale(domain.Nodes().size()); for (std::size_t node = 0U; node < constraint_hessians.size(); ++node) { for (const auto& entry : constraint_hessians[node]) { const double term = entry.second * frame.velocity[entry.first.first] * frame.velocity[entry.first.second]; hidden_second[node] += term; hidden_second_scale[node] += std::abs(term); } } return Result::Success( {std::move(frame), std::move(mass.Value()), std::move(stiffness.Value()), std::move(effective.Value()), std::move(metric.Value()), std::move(third), std::move(third_scale), std::move(rotations), momentum_scale, angular_scale, std::move(hidden_second), std::move(hidden_second_scale)}); } Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs, const DynamicAssembly& assembly, DynamicAcceptedFrameState* frame) { static_cast(dofs); Vector3 total_force; Vector3 total_moment; double force_scale = assembly.momentum_rate_scale; double moment_scale = assembly.angular_momentum_rate_scale; for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { const auto first = 6U * node; const Vector3 arm = Vector3(domain.Nodes()[node].coordinates) + Triple(frame->generalized_coordinates, first); const Matrix transform = InverseTransposeLeftJacobianSO3( Triple(frame->generalized_coordinates, first + 3U)); for (const Vector* values : {&frame->external_force, &frame->reaction}) { const Vector3 force = Triple(*values, first); const Vector3 moment = Transform(transform, Triple(*values, first + 3U)); total_force = total_force + force; total_moment = total_moment + arm.Cross(force) + moment; force_scale += force.Norm(); moment_scale += arm.Cross(force).Norm() + moment.Norm(); } } const Vector3 force_error = total_force - Vector3(frame->linear_momentum_rate); const Vector3 moment_error = total_moment - Vector3(frame->angular_momentum_rate); for (std::size_t c = 0U; c < 3U; ++c) { frame->global_equilibrium[c] = force_error[c]; frame->global_equilibrium[3U + c] = moment_error[c]; } if (!force_error.IsFinite() || !moment_error.IsFinite() || !std::isfinite(force_scale) || !std::isfinite(moment_scale) || Ratio(force_error.Norm(), force_scale) > 1e-8 || Ratio(moment_error.Norm(), moment_scale) > 1e-8) return Failure("Dynamic physical force or moment balance exceeds 1e-8."); return Status::Ok(); } } // namespace fesa::hht_internal