#include #include #include #include #include #include #include #include #include #include #include #include #include "analysis/hht_dynamic_internal.h" #include "fesa/math/matrix.h" #include "fesa/solvers/linear/linear_solver.h" #include "fesa/solvers/linear/tangent_solver.h" #include "math/dense_blas_internal.h" #include "math/compensated_sum.h" namespace fesa { namespace hht_internal { namespace { using math_internal::AddCompensated; /// @brief Attaches a fail-closed dynamic diagnostic to the source model. Status Failure(const Domain& domain, const std::string& code, const std::string& message) { return Status::Failure(FailureCategory::kSolver, {{Severity::kError, code, {domain.SourcePath(), 0U}, "*DYNAMIC", {}, message}}); } /// @brief Reads one structural entry, including an implicit exact zero. double Entry(const SparseMatrix& matrix, const std::size_t row, const std::size_t column) { const auto first = matrix.ColumnIndices().begin() + static_cast(matrix.RowOffsets()[row]); const auto last = matrix.ColumnIndices().begin() + static_cast(matrix.RowOffsets()[row + 1U]); const auto found = std::lower_bound(first, last, column); return found != last && *found == column ? matrix.Values()[static_cast( found - matrix.ColumnIndices().begin())] : 0.0; } /// @brief Applies the fixed translation length scale by full-space row. double Scale(const std::size_t full_dof, const double length) { return full_dof % 6U < 3U ? length : 1.0; } /// @brief Checks a dimensionless ratio without inventing a zero-scale floor. bool Within(const double value, const double scale, const double tolerance) { return std::isfinite(value) && std::isfinite(scale) && scale >= 0.0 && (scale == 0.0 ? value == 0.0 : std::abs(value) / scale <= tolerance); } /// @brief Verifies dimensions and finite values before algebraic contractions. Status Validate(const Domain& domain, const DofManager& dofs, const DynamicAssembly& assembly, const double length) { const std::size_t count = dofs.FullDofCount(); if (!std::isfinite(length) || !(length > 0.0) || count % 6U != 0U || assembly.rotations.size() != count / 6U) { return Failure(domain, "dynamic-invalid-algebra-state", "Dynamic algebra requires complete rotations and a positive " "reference length."); } for (const Vector* vector : {&assembly.frame.velocity, &assembly.frame.acceleration, &assembly.frame.internal_force, &assembly.frame.external_force, &assembly.frame.inertia_force, &assembly.frame.residual_scale, &assembly.force_directional_second, &assembly.force_directional_second_scale}) { if (vector->Size() != count) { return Failure( domain, "dynamic-invalid-algebra-state", "Dynamic algebra vectors must match the full equation space."); } for (std::size_t i = 0; i < count; ++i) { if (!std::isfinite((*vector)[i])) { return Failure(domain, "dynamic-invalid-algebra-state", "Dynamic algebra vectors must remain finite."); } } } for (const Vector* vector : {&assembly.hidden_second, &assembly.hidden_second_scale}) { if (vector->Size() != assembly.rotations.size()) { return Failure( domain, "dynamic-invalid-algebra-state", "Exact scalar hidden Hessian evidence must cover every node."); } for (std::size_t i = 0; i < vector->Size(); ++i) { if (!std::isfinite((*vector)[i]) || (vector == &assembly.hidden_second_scale && (*vector)[i] < 0.0)) { return Failure(domain, "dynamic-invalid-algebra-state", "Scalar hidden Hessian evidence must be finite with " "nonnegative scale."); } } } for (const SparseMatrix* matrix : {&assembly.mass, &assembly.internal_tangent, &assembly.drilling_metric}) { if (matrix->Rows() != count || matrix->Columns() != count || !matrix->Validate().IsOk()) { return Failure( domain, "dynamic-invalid-algebra-state", "Dynamic algebra operators must match the full equation space."); } } for (const auto& rotation : assembly.rotations) { if (!rotation.drill_null.IsFinite() || !(std::abs(rotation.drill_null.Dot(rotation.drill_null) - 1.0) <= 1.0e-12)) { return Failure( domain, "dynamic-invalid-analytic-null", "Every analytic drilling null must remain finite and unit length."); } } return Status::Ok(); } using BasisRow = std::vector>; /// @brief Holds sparse nodal physical-complement columns and their mass. struct PhysicalSpace { std::vector rows; SparseMatrix mass; }; /// @brief Checks a nonempty nodal physical block without removing small pivots. Status CheckLocalMass(const Domain& domain, const SparseMatrix& matrix, const std::size_t begin, const std::size_t end) { if (begin == end) return Status::Ok(); const std::size_t size = end - begin; const dense_blas_internal::ScopedLocalDenseThreads local_threads{}; Matrix block{size, size}; double scale = 0.0; for (std::size_t i = 0; i < size; ++i) { for (std::size_t j = 0; j < size; ++j) { block(i, j) = Entry(matrix, begin + i, begin + j); scale = std::max(scale, std::abs(block(i, j))); } } if (!std::isfinite(scale) || !(scale > 0.0)) { return Failure(domain, "dynamic-positive-mass", "A nonempty physical mass block has no positive scale."); } for (std::size_t i = 0; i < size; ++i) for (std::size_t j = 0; j < size; ++j) block(i, j) /= scale; try { const auto factor = dense_blas_internal::DenseFactor::Cholesky(block); if (!factor.HasValue()) return Failure( domain, "dynamic-positive-mass", "Physical mass requires strictly positive Cholesky pivots."); const auto rcond = factor.Value().ReciprocalConditionInfinity(); if (!rcond.HasValue() || !std::isfinite(rcond.Value()) || !(rcond.Value() >= std::ldexp(1.0, -32))) return Failure( domain, "dynamic-positive-mass-conditioning", "A nodal free physical block reciprocal condition is below 2^-32."); } catch (const std::exception& error) { return Failure(domain, "dynamic-positive-mass", error.what()); } return Status::Ok(); } /// @brief Builds the actual free-space analytic complement, without rank cuts. Result BuildPhysicalSpace( const Domain& domain, const DofManager& dofs, const DynamicAssembly& assembly, const double length, const std::vector& null_nodes, LinearSolver& solver) { const auto scaled = ScaledFree(assembly.mass, dofs, length); if (!scaled.HasValue()) return Result::Failure(scaled.GetStatus()); const SparseMatrix& mass = scaled.Value(); double mass_scale = 0.0; for (const double entry : mass.Values()) mass_scale = std::max(mass_scale, std::abs(entry)); const std::size_t no_null = std::numeric_limits::max(); std::vector node_null(assembly.rotations.size(), no_null); for (std::size_t i = 0; i < null_nodes.size(); ++i) node_null[null_nodes[i]] = i; double mass_norm = 0.0; double symmetry_error = 0.0; double null_error = 0.0; if (mass_scale > 0.0) { Vector normalized_mass{mass.Values().size()}; Vector null_coefficients{mass.Values().size()}; for (std::size_t index = 0U; index < mass.Values().size(); ++index) { normalized_mass[index] = mass.Values()[index] / mass_scale; const auto full = dofs.FreeDofs()[mass.ColumnIndices()[index]]; if (full % 6U >= 3U && node_null[full / 6U] != no_null) null_coefficients[index] = assembly.rotations[full / 6U].drill_null[full % 6U - 3U]; } Vector null_products{0U}; { const dense_blas_internal::ScopedLocalDenseThreads local_threads{}; null_products = dense_blas_internal::MultiplyDiagonal(normalized_mass, null_coefficients); } for (std::size_t row = 0; row < mass.Rows(); ++row) { std::vector action(null_nodes.size(), 0.0); for (std::size_t index = mass.RowOffsets()[row]; index < mass.RowOffsets()[row + 1U]; ++index) { const std::size_t column = mass.ColumnIndices()[index]; const double entry = normalized_mass[index]; mass_norm = std::hypot(mass_norm, entry); symmetry_error = std::hypot( symmetry_error, entry - Entry(mass, column, row) / mass_scale); const std::size_t full = dofs.FreeDofs()[column]; if (full % 6U >= 3U && node_null[full / 6U] != no_null) { action[node_null[full / 6U]] += null_products[index]; } } for (const double value : action) null_error = std::hypot(null_error, value); } } if ((mass_norm > 0.0 && symmetry_error / mass_norm > 1.0e-12) || (!null_nodes.empty() && mass_norm > 0.0 && null_error / mass_norm / std::sqrt(static_cast(null_nodes.size())) > 1.0e-12)) { return Result::Failure( Failure(domain, "dynamic-invalid-mass-null", "Scaled free mass symmetry or analytic null action failed.")); } std::vector rows(dofs.FullDofCount()); std::vector node_starts; std::size_t dimension = 0U; for (std::size_t node = 0; node < assembly.rotations.size(); ++node) { node_starts.push_back(dimension); for (std::size_t component = 0; component < 3; ++component) { if (dofs.FreeEquation(6U * node + component).has_value()) { rows[6U * node + component].push_back({dimension++, 1.0}); } } std::vector free_rotations; for (std::size_t component = 0; component < 3; ++component) { if (dofs.FreeEquation(6U * node + 3U + component).has_value()) { free_rotations.push_back(component); } } if (node_null[node] == no_null) { for (const std::size_t component : free_rotations) { rows[6U * node + 3U + component].push_back({dimension++, 1.0}); } continue; } const Vector3 n = assembly.rotations[node].drill_null; std::stable_sort(free_rotations.begin(), free_rotations.end(), [&](const std::size_t a, const std::size_t b) { return std::abs(n[a]) < std::abs(n[b]); }); std::vector basis; for (std::size_t column = 0; column + 1U < free_rotations.size(); ++column) { std::array axis{}; axis[free_rotations[column]] = 1.0; Vector3 tangent = Vector3{axis} - n[free_rotations[column]] * n; for (const Vector3& previous : basis) tangent = tangent - tangent.Dot(previous) * previous; const double norm = tangent.Norm(); if (!std::isfinite(norm) || !(norm > 0.0)) { return Result::Failure(Failure( domain, "dynamic-invalid-analytic-null", "The free physical complement lost its analytic dimension.")); } tangent = tangent / norm; basis.push_back(tangent); for (const std::size_t component : free_rotations) { rows[6U * node + 3U + component].push_back( {dimension, tangent[component]}); } ++dimension; } } std::vector entries; std::vector> mass_factors; node_starts.push_back(dimension); double complement_gram_error = 0.0; double null_gram_error = 0.0; double cross_gram_error = 0.0; for (std::size_t node = 0; node < assembly.rotations.size(); ++node) { const std::size_t first = node_starts[node]; const std::size_t last = node_starts[node + 1U]; std::size_t free_count = 0U; for (std::size_t c = 0; c < 6; ++c) { if (dofs.FreeEquation(6U * node + c).has_value()) ++free_count; } const bool has_null = node_null[node] != no_null; if (last - first + (has_null ? 1U : 0U) != free_count) { return Result::Failure(Failure( domain, "dynamic-invalid-analytic-null", "The free physical and null bases do not span the free space.")); } for (std::size_t a = first; a < last; ++a) { for (std::size_t b = first; b < last; ++b) { double gram = a == b ? -1.0 : 0.0; for (std::size_t c = 0; c < 6; ++c) { double pa = 0.0; double pb = 0.0; for (const auto& entry : rows[6U * node + c]) { if (entry.first == a) pa = entry.second; if (entry.first == b) pb = entry.second; } gram += pa * pb; } complement_gram_error = std::hypot(complement_gram_error, gram); } if (has_null) { double gram = 0.0; for (std::size_t c = 0; c < 3; ++c) { for (const auto& entry : rows[6U * node + 3U + c]) { if (entry.first == a) gram += entry.second * assembly.rotations[node].drill_null[c]; } } cross_gram_error = std::hypot(cross_gram_error, gram); } } if (has_null) null_gram_error = std::hypot(null_gram_error, assembly.rotations[node].drill_null.Dot( assembly.rotations[node].drill_null) - 1.0); } const double full_gram_error = std::hypot( complement_gram_error, std::hypot(null_gram_error, std::sqrt(2.0) * cross_gram_error)); if (!(cross_gram_error <= 1.0e-12) || !(full_gram_error <= 1.0e-12) || (!null_nodes.empty() && !(null_gram_error / std::sqrt(static_cast(null_nodes.size())) <= 1.0e-12))) { return Result::Failure(Failure( domain, "dynamic-invalid-analytic-null", "The scaled physical and analytic null Gram identities failed.")); } for (std::size_t i = 0; i < dimension; ++i) { entries.push_back({i, i, 0.0, 0U, entries.size()}); mass_factors.push_back({0.0, 0.0}); } for (std::size_t row = 0; row < mass.Rows(); ++row) { const auto& left = rows[dofs.FreeDofs()[row]]; for (std::size_t index = mass.RowOffsets()[row]; index < mass.RowOffsets()[row + 1U]; ++index) { const auto& right = rows[dofs.FreeDofs()[mass.ColumnIndices()[index]]]; for (const auto& a : left) { for (const auto& b : right) { entries.push_back({a.first, b.first, a.second, 0U, entries.size()}); mass_factors.push_back({mass.Values()[index], b.second}); } } } } Vector coefficients{entries.size()}; Vector mass_values{entries.size()}; Vector right_values{entries.size()}; for (std::size_t i = 0U; i < entries.size(); ++i) { coefficients[i] = entries[i].value; mass_values[i] = mass_factors[i].first; right_values[i] = mass_factors[i].second; } Vector projected{0U}; { const dense_blas_internal::ScopedLocalDenseThreads local_threads{}; projected = dense_blas_internal::MultiplyDiagonal(coefficients, mass_values); bool finite = true; for (std::size_t i = 0U; i < projected.Size(); ++i) { entries[i].value = projected[i]; finite = finite && std::isfinite(projected[i]); } // A rejected intermediate cannot produce a finite original contribution. // Still compute the remaining finite entries so an earlier final-product // overflow keeps its diagnostic priority. No invalid operand reaches MKL. if (!finite) { std::vector valid_positions; for (std::size_t i = 0U; i < projected.Size(); ++i) if (std::isfinite(projected[i])) valid_positions.push_back(i); Vector valid_left{valid_positions.size()}; Vector valid_right{valid_positions.size()}; for (std::size_t i = 0U; i < valid_positions.size(); ++i) { valid_left[i] = projected[valid_positions[i]]; valid_right[i] = right_values[valid_positions[i]]; } const Vector valid_products = dense_blas_internal::MultiplyDiagonal(valid_left, valid_right); for (std::size_t i = 0U; i < valid_positions.size(); ++i) entries[valid_positions[i]].value = valid_products[i]; auto rejected = MakeSparse(dimension, dimension, std::move(entries)); return Result::Failure(rejected.GetStatus()); } projected = dense_blas_internal::MultiplyDiagonal(projected, right_values); } for (std::size_t i = 0U; i < entries.size(); ++i) entries[i].value = projected[i]; auto block = MakeSparse(dimension, dimension, std::move(entries)); if (!block.HasValue()) return Result::Failure(block.GetStatus()); for (std::size_t node = 0; node < assembly.rotations.size(); ++node) { const Status local = CheckLocalMass( domain, block.Value(), node_starts[node], node_starts[node + 1U]); if (!local.IsOk()) return Result::Failure(local); } if (dimension > 0U) { const Status factor = solver.Factorize(block.Value()); if (!factor.IsOk()) return Result::Failure(factor); } return Result::Success( {std::move(rows), std::move(block.Value())}); } /// @brief Selects the same scalar-primitive gradient used by the reduced graph. Result ConstraintGradient(const DynamicAssembly& assembly, const std::vector& nodes) { if (!nodes.empty() && !assembly.hidden_gradient.has_value()) return Result::Failure(Status::Failure( FailureCategory::kSolver, {{Severity::kError, "dynamic-algebraic-gradient", {}, "DYNAMIC", {}, "Canonical drilling-constraint gradient was not assembled."}})); std::vector entries; for (std::size_t row = 0U; row < nodes.size(); ++row) { const auto& gradient = *assembly.hidden_gradient; const auto node = nodes[row]; if (node >= gradient.Rows() || gradient.Columns() != assembly.frame.acceleration.Size()) return Result::Failure(Status::Failure( FailureCategory::kSolver, {{Severity::kError, "dynamic-algebraic-gradient", {}, "DYNAMIC", {}, "Canonical drilling-constraint gradient dimensions differ."}})); for (auto index = gradient.RowOffsets()[node]; index < gradient.RowOffsets()[node + 1U]; ++index) entries.push_back({row, gradient.ColumnIndices()[index], gradient.Values()[index], 0U, entries.size()}); } return MakeSparse(nodes.size(), assembly.internal_tangent.Columns(), std::move(entries)); } /// @brief Stores W-scaled regularity with exact inverse-action infinity norms. struct Regularity { SparseMatrix matrix; Vector weights; }; /// @brief Factors W^-1 psi_q N W^-1 and checks the prescribed condition bound. Result CheckRegularity(const Domain& domain, const DynamicAssembly& assembly, const std::vector& nodes, const SparseMatrix& gradient, TangentSolver& solver) { const std::size_t no_null = std::numeric_limits::max(); std::vector node_null(assembly.rotations.size(), no_null); Vector weights{nodes.size()}; for (std::size_t i = 0; i < nodes.size(); ++i) { node_null[nodes[i]] = i; const auto& n = assembly.rotations[nodes[i]].drill_null; const double stiffness = OrderedNullMetric( assembly.drilling_metric, nodes[i], n); if (!std::isfinite(stiffness) || !(stiffness > 0.0)) { return Result::Failure( Failure(domain, "dynamic-algebraic-regularity", "Every admissible drill null requires positive finite " "drilling metric.")); } weights[i] = std::sqrt(stiffness); } std::vector entries; std::vector null_coefficients; for (std::size_t row = 0; row < nodes.size(); ++row) { entries.push_back({row, row, 0.0, 0U, entries.size()}); null_coefficients.push_back(0.0); for (std::size_t index = gradient.RowOffsets()[row]; index < gradient.RowOffsets()[row + 1U]; ++index) { const std::size_t full = gradient.ColumnIndices()[index]; if (full % 6U >= 3U && node_null[full / 6U] != no_null) { const std::size_t column = node_null[full / 6U]; entries.push_back( {row, column, gradient.Values()[index], 0U, entries.size()}); null_coefficients.push_back( assembly.rotations[full / 6U].drill_null[full % 6U - 3U]); } } } Vector gradient_values{entries.size()}; Vector null_values{entries.size()}; for (std::size_t i = 0U; i < entries.size(); ++i) { gradient_values[i] = entries[i].value; null_values[i] = null_coefficients[i]; } Vector products{0U}; { const dense_blas_internal::ScopedLocalDenseThreads local_threads{}; products = dense_blas_internal::MultiplyDiagonal(gradient_values, null_values); } for (std::size_t i = 0U; i < entries.size(); ++i) entries[i].value = products[i] / weights[entries[i].row] / weights[entries[i].column]; auto matrix = MakeSparse(nodes.size(), nodes.size(), std::move(entries)); if (!matrix.HasValue()) return Result::Failure(matrix.GetStatus()); if (nodes.empty()) return Result::Success( {std::move(matrix.Value()), std::move(weights)}); const Status factor = solver.FactorizeFresh(matrix.Value()); if (!factor.IsOk()) return Result::Failure(factor); double norm = 0.0; for (std::size_t row = 0; row < nodes.size(); ++row) { double sum = 0.0; for (std::size_t index = matrix.Value().RowOffsets()[row]; index < matrix.Value().RowOffsets()[row + 1U]; ++index) sum += std::abs(matrix.Value().Values()[index]); norm = std::max(norm, sum); } double inverse_norm = 0.0; const Status accumulated = AccumulateCheckedCanonicalNorm(matrix.Value(), solver, inverse_norm); if (!accumulated.IsOk()) return Result::Failure(accumulated); const double condition = 1.0 / norm / inverse_norm; if (!std::isfinite(norm) || !std::isfinite(inverse_norm) || !std::isfinite(condition) || !(condition >= std::ldexp(1.0, -32))) { std::ostringstream message; message << std::setprecision(17) << "Scaled null regularity reciprocal infinity condition " << condition << " is below 2^-32; norm=" << norm << ", inverse-action norm=" << inverse_norm << "."; return Result::Failure( Failure(domain, "dynamic-algebraic-conditioning", message.str())); } return Result::Success( {std::move(matrix.Value()), std::move(weights)}); } /// @brief Recovers raw hidden equations and uncancelled derivative term scales. Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly, const std::vector& nodes, const SparseMatrix& gradient, DynamicAcceptedFrameState* frame, const bool require_acceleration) { const bool direct = frame->mitc4_drilling && frame->mitc4_drilling->mode == Mitc4DrillingMode::kDirectSpring; if (direct) { for (const std::size_t node : nodes) { if (node >= assembly.hidden_constraint.Size()) return Failure(domain, "dynamic-invalid-algebra-state", "Canonical hidden values must cover every admissible null node."); if (!std::isfinite(assembly.hidden_constraint[node])) return Failure(domain, "dynamic-invalid-algebra-state", "Canonical hidden values must remain finite."); } } Vector psi{nodes.size()}; Vector psi_scale{nodes.size()}; Vector velocity_defect{nodes.size()}; Vector acceleration_defect{nodes.size()}; Vector velocity_scale{nodes.size()}; Vector acceleration_scale{nodes.size()}; for (std::size_t row = 0; row < nodes.size(); ++row) { const std::size_t node = nodes[row]; const auto& rotation = assembly.rotations[node]; double velocity_correction = 0.0; double acceleration_correction = 0.0; for (std::size_t index = gradient.RowOffsets()[row]; index < gradient.RowOffsets()[row + 1U]; ++index) { const std::size_t column = gradient.ColumnIndices()[index]; AddCompensated(gradient.Values()[index] * frame->velocity[column], &velocity_defect[row], &velocity_correction); AddCompensated(gradient.Values()[index] * frame->acceleration[column], &acceleration_defect[row], &acceleration_correction); velocity_scale[row] += std::abs(gradient.Values()[index] * frame->velocity[column]); acceleration_scale[row] += std::abs(gradient.Values()[index] * frame->acceleration[column]); } for (std::size_t c = 0; c < 3; ++c) { const std::size_t full = 6U * node + 3U + c; const double g = frame->internal_force[full] - frame->external_force[full]; const double n = rotation.drill_null[c]; if (!direct) psi[row] += n * g; psi_scale[row] += std::abs(n) * frame->residual_scale[full]; } if (direct) psi[row] = assembly.hidden_constraint[node]; velocity_defect[row] += velocity_correction; AddCompensated(assembly.hidden_second[node], &acceleration_defect[row], &acceleration_correction); acceleration_defect[row] += acceleration_correction; acceleration_scale[row] += assembly.hidden_second_scale[node]; if (!Within(psi[row], psi_scale[row], 1.0e-10) || (frame->is_initial && !Within(velocity_defect[row], velocity_scale[row], 1.0e-10)) || (frame->is_initial && require_acceleration && !Within(acceleration_defect[row], acceleration_scale[row], 1.0e-10)) || !std::isfinite(velocity_defect[row]) || !std::isfinite(acceleration_defect[row]) || !std::isfinite(velocity_scale[row]) || !std::isfinite(acceleration_scale[row])) { std::ostringstream message; message << std::setprecision(17) << "Dynamic null consistency failed at node " << domain.Nodes()[node].source_id.instance_name << ":" << domain.Nodes()[node].source_id.source_label_text << "; psi=" << psi[row] << ", psi_scale=" << psi_scale[row] << ", velocity=" << velocity_defect[row] << ", velocity_scale=" << velocity_scale[row] << ", acceleration=" << acceleration_defect[row] << ", acceleration_scale=" << acceleration_scale[row] << ", initial=" << frame->is_initial << ", enforce_acceleration=" << require_acceleration << "."; return Failure(domain, "dynamic-hidden-constraint", message.str()); } } frame->hidden_constraint = std::move(psi); frame->hidden_constraint_scale = std::move(psi_scale); frame->hidden_velocity_defect = std::move(velocity_defect); frame->hidden_velocity_defect_scale = std::move(velocity_scale); frame->hidden_acceleration_defect = std::move(acceleration_defect); frame->hidden_acceleration_defect_scale = std::move(acceleration_scale); return Status::Ok(); } } // namespace Status AccumulateCheckedCanonicalNorm(const SparseMatrix& matrix, TangentSolver& solver, double& norm) { const auto size = matrix.Rows(); Vector row_sums{size}; for (std::size_t column = 0U; column < size; ++column) { Vector rhs{size}; rhs[column] = 1.0; Vector solution{size}; const Status solved = solver.SolveCorrection(rhs, solution); if (!solved.IsOk()) return solved; const Status checked = CheckLinearSolve(matrix, rhs, solution); if (!checked.IsOk()) return checked; for (std::size_t row = 0U; row < size; ++row) row_sums[row] += std::abs(solution[row]); } for (std::size_t row = 0U; row < size; ++row) norm = (std::max)(norm, row_sums[row]); return Status::Ok(); } double OrderedNullMetric(const SparseMatrix& metric, const std::size_t node, const Vector3& direction) { Vector left{9U}; Vector entries{9U}; Vector right{9U}; for (std::size_t i = 0U; i < 3U; ++i) for (std::size_t j = 0U; j < 3U; ++j) { const auto index = 3U * i + j; left[index] = direction[i]; entries[index] = Entry(metric, 6U * node + 3U + i, 6U * node + 3U + j); right[index] = direction[j]; if (!std::isfinite(left[index]) || !std::isfinite(entries[index]) || !std::isfinite(right[index])) return std::numeric_limits::quiet_NaN(); } const dense_blas_internal::ScopedLocalDenseThreads local_threads{}; const Vector first = dense_blas_internal::MultiplyDiagonal(left, entries); for (std::size_t i = 0U; i < first.Size(); ++i) if (!std::isfinite(first[i])) return std::numeric_limits::quiet_NaN(); const Vector products = dense_blas_internal::MultiplyDiagonal(first, right); double value = 0.0; for (std::size_t i = 0U; i < products.Size(); ++i) value += products[i]; return value; } std::vector NullNodes( const DofManager& dofs, const std::vector& rotations) { std::vector nodes; for (std::size_t node = 0; node < rotations.size(); ++node) { bool admissible = true; for (std::size_t component = 0; component < 3; ++component) { if (!dofs.FreeEquation(6U * node + 3U + component).has_value() && rotations[node].drill_null[component] != 0.0) admissible = false; } if (admissible) nodes.push_back(node); } return nodes; } Result InitializeAcceleration(const Domain& domain, const DofManager& dofs, const DynamicAssembly& initial, const double length, LinearSolver& positive_solver, TangentSolver& tangent_solver) { const Status valid = Validate(domain, dofs, initial, length); if (!valid.IsOk()) return Result::Failure(valid); const auto nodes = NullNodes(dofs, initial.rotations); auto physical = BuildPhysicalSpace(domain, dofs, initial, length, nodes, positive_solver); if (!physical.HasValue()) return Result::Failure(physical.GetStatus()); auto gradient = ConstraintGradient(initial, nodes); if (!gradient.HasValue()) return Result::Failure(gradient.GetStatus()); auto regularity = CheckRegularity(domain, initial, nodes, gradient.Value(), tangent_solver); if (!regularity.HasValue()) return Result::Failure(regularity.GetStatus()); DynamicAcceptedFrameState candidate = initial.frame; candidate.is_initial = true; candidate.acceleration = Vector{dofs.FullDofCount()}; const Status consistent = HiddenEvidence(domain, initial, nodes, gradient.Value(), &candidate, false); if (!consistent.IsOk()) return Result::Failure(consistent); Vector rhs{physical.Value().mass.Rows()}; // The initialization evaluation has zero acceleration, so I(q,v,0)=c(q,v). for (std::size_t full = 0; full < dofs.FullDofCount(); ++full) { if (initial.frame.acceleration[full] != 0.0) { return Result::Failure(Failure( domain, "dynamic-invalid-initial-acceleration", "Initial inertia must be evaluated at zero unknown acceleration.")); } const double force = initial.frame.external_force[full] - initial.frame.internal_force[full] - initial.frame.inertia_force[full]; for (const auto& entry : physical.Value().rows[full]) { rhs[entry.first] += entry.second * Scale(full, length) * force; } } Vector solution{rhs.Size()}; if (rhs.Size() > 0U) { const Status solved = positive_solver.Solve(rhs, solution); if (!solved.IsOk()) return Result::Failure(solved); const Status checked = CheckLinearSolve(physical.Value().mass, rhs, solution); if (!checked.IsOk()) return Result::Failure(checked); } for (std::size_t full = 0; full < dofs.FullDofCount(); ++full) { for (const auto& entry : physical.Value().rows[full]) { candidate.acceleration[full] += Scale(full, length) * entry.second * solution[entry.first]; } } const Status physical_hidden = HiddenEvidence( domain, initial, nodes, gradient.Value(), &candidate, false); if (!physical_hidden.IsOk()) return Result::Failure(physical_hidden); if (!nodes.empty()) { Vector null_rhs{nodes.size()}; for (std::size_t row = 0; row < nodes.size(); ++row) { null_rhs[row] = -candidate.hidden_acceleration_defect[row] / regularity.Value().weights[row]; } Vector correction{nodes.size()}; const Status solved = tangent_solver.SolveCorrection(null_rhs, correction); if (!solved.IsOk()) return Result::Failure(solved); const Status checked = CheckLinearSolve(regularity.Value().matrix, null_rhs, correction); if (!checked.IsOk()) return Result::Failure(checked); for (std::size_t i = 0; i < nodes.size(); ++i) { for (std::size_t c = 0; c < 3; ++c) { candidate.acceleration[6U * nodes[i] + 3U + c] += initial.rotations[nodes[i]].drill_null[c] * correction[i] / regularity.Value().weights[i]; } } } const Status hidden = HiddenEvidence(domain, initial, nodes, gradient.Value(), &candidate, true); if (!hidden.IsOk()) return Result::Failure(hidden); for (std::size_t full = 0U; full < candidate.acceleration.Size(); ++full) { if (!std::isfinite(candidate.acceleration[full])) { return Result::Failure(Failure( domain, "dynamic-invalid-initial-acceleration", "Initial acceleration must be finite.")); } } for (const std::size_t full : dofs.ConstrainedDofs()) { if (candidate.acceleration[full] != 0.0) return Result::Failure(Failure( domain, "dynamic-invalid-initial-acceleration", "Initial acceleration must be exactly zero at constrained DOFs.")); } return Result::Success(std::move(candidate.acceleration)); } Status CompleteConstraintEvidence( const Domain& domain, const DofManager& dofs, const DynamicAssembly& assembly, const double length, const std::vector& initial_null_nodes, LinearSolver& positive_solver, TangentSolver& tangent_solver, DynamicAcceptedFrameState* frame) { const Status valid = Validate(domain, dofs, assembly, length); if (!valid.IsOk()) return valid; const auto nodes = NullNodes(dofs, assembly.rotations); if (nodes != initial_null_nodes) { return Failure(domain, "dynamic-mass-null-rank-change", "The admissible analytic drilling-null inventory changed."); } const auto physical = BuildPhysicalSpace(domain, dofs, assembly, length, nodes, positive_solver); if (!physical.HasValue()) return physical.GetStatus(); const auto gradient = ConstraintGradient(assembly, nodes); if (!gradient.HasValue()) return gradient.GetStatus(); const auto regularity = CheckRegularity(domain, assembly, nodes, gradient.Value(), tangent_solver); if (!regularity.HasValue()) return regularity.GetStatus(); DynamicAcceptedFrameState candidate = *frame; const Status hidden = HiddenEvidence(domain, assembly, nodes, gradient.Value(), &candidate, true); if (!hidden.IsOk()) return hidden; *frame = std::move(candidate); return Status::Ok(); } } // namespace hht_internal } // namespace fesa