#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" namespace fesa { namespace hht_internal { namespace { /// @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; std::array, 6> block{}; std::array, 6> lower{}; 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."); } double norm = 0.0; for (std::size_t i = 0; i < size; ++i) { double sum = 0.0; for (std::size_t j = 0; j < size; ++j) { block[i][j] /= scale; sum += std::abs(block[i][j]); } norm = std::max(norm, sum); for (std::size_t j = 0; j <= i; ++j) { double value = block[i][j]; for (std::size_t k = 0; k < j; ++k) value -= lower[i][k] * lower[j][k]; if (!std::isfinite(value) || (i == j && !(value > 0.0))) { return Failure( domain, "dynamic-positive-mass", "Physical mass requires strictly positive Cholesky pivots."); } lower[i][j] = i == j ? std::sqrt(value) : value / lower[j][j]; } } std::array sums{}; for (std::size_t column = 0; column < size; ++column) { std::array solution{}; for (std::size_t i = 0; i < size; ++i) { double value = i == column ? 1.0 : 0.0; for (std::size_t k = 0; k < i; ++k) value -= lower[i][k] * solution[k]; solution[i] = value / lower[i][i]; } for (std::size_t reverse = size; reverse > 0U; --reverse) { const std::size_t i = reverse - 1U; double value = solution[i]; for (std::size_t k = i + 1U; k < size; ++k) value -= lower[k][i] * solution[k]; solution[i] = value / lower[i][i]; sums[i] += std::abs(solution[i]); } } const double inverse_norm = *std::max_element(sums.begin(), sums.end()); const double rcond = 1.0 / norm / inverse_norm; if (!std::isfinite(inverse_norm) || !std::isfinite(rcond) || !(rcond >= std::ldexp(1.0, -32))) { return Failure( domain, "dynamic-positive-mass-conditioning", "A nodal free physical block reciprocal condition is below 2^-32."); } 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) { 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 = mass.Values()[index] / mass_scale; 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]] += entry * assembly.rotations[full / 6U].drill_null[full % 6U - 3U]; } } 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; 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()}); 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 * mass.Values()[index] * b.second, 0U, entries.size()}); } } } } 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 Builds psi_q from the current unit-null derivatives and force /// Hessian. Result ConstraintGradient(const DynamicAssembly& assembly, const std::vector& nodes) { std::vector entries; const auto& stiffness = assembly.internal_tangent; for (std::size_t row = 0; row < nodes.size(); ++row) { const std::size_t node = nodes[row]; const auto& rotation = assembly.rotations[node]; for (std::size_t c = 0; c < 3; ++c) { const std::size_t full = 6U * node + 3U + c; const double g = assembly.frame.internal_force[full] - assembly.frame.external_force[full]; for (std::size_t d = 0; d < 3; ++d) { entries.push_back({row, 6U * node + 3U + d, rotation.null_gradient(c, d) * g, 0U, entries.size()}); } for (std::size_t index = stiffness.RowOffsets()[full]; index < stiffness.RowOffsets()[full + 1U]; ++index) { entries.push_back({row, stiffness.ColumnIndices()[index], rotation.drill_null[c] * stiffness.Values()[index], 0U, entries.size()}); } } } return MakeSparse(nodes.size(), stiffness.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; double stiffness = 0.0; for (std::size_t a = 0; a < 3; ++a) { for (std::size_t b = 0; b < 3; ++b) { stiffness += n[a] * Entry(assembly.drilling_metric, 6U * nodes[i] + 3U + a, 6U * nodes[i] + 3U + b) * n[b]; } } 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; for (std::size_t row = 0; row < nodes.size(); ++row) { entries.push_back({row, row, 0.0, 0U, entries.size()}); 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] * assembly.rotations[full / 6U].drill_null[full % 6U - 3U] / weights[row] / weights[column], 0U, entries.size()}); } } } 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); } Vector inverse_sums{nodes.size()}; // Columns are discarded after their absolute row sums are accumulated; // a global dense inverse is never retained. for (std::size_t column = 0; column < nodes.size(); ++column) { Vector rhs{nodes.size()}; rhs[column] = 1.0; Vector solution{nodes.size()}; const Status solved = solver.SolveCorrection(rhs, solution); if (!solved.IsOk()) return Result::Failure(solved); const Status checked = CheckLinearSolve(matrix.Value(), rhs, solution); if (!checked.IsOk()) return Result::Failure(checked); for (std::size_t row = 0; row < nodes.size(); ++row) inverse_sums[row] += std::abs(solution[row]); } double inverse_norm = 0.0; for (std::size_t row = 0; row < nodes.size(); ++row) inverse_norm = std::max(inverse_norm, inverse_sums[row]); 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) { Vector psi{nodes.size()}; Vector psi_scale{nodes.size()}; Vector velocity_defect = gradient.Multiply(frame->velocity); Vector acceleration_defect = gradient.Multiply(frame->acceleration); 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]; for (std::size_t index = gradient.RowOffsets()[row]; index < gradient.RowOffsets()[row + 1U]; ++index) { const std::size_t column = gradient.ColumnIndices()[index]; 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]; psi[row] += n * g; psi_scale[row] += std::abs(n) * frame->residual_scale[full]; } acceleration_defect[row] += assembly.hidden_second[node]; 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 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); const Vector ma = initial.mass.Multiply(candidate.acceleration); for (const std::size_t full : dofs.FreeDofs()) { const double residual = ma[full] + initial.frame.inertia_force[full] + initial.frame.internal_force[full] - initial.frame.external_force[full]; double scale = std::abs(initial.frame.inertia_force[full]) + std::abs(initial.frame.internal_force[full]) + std::abs(initial.frame.external_force[full]); for (std::size_t index = initial.mass.RowOffsets()[full]; index < initial.mass.RowOffsets()[full + 1U]; ++index) { scale += std::abs(initial.mass.Values()[index] * candidate.acceleration[initial.mass.ColumnIndices()[index]]); } if (!Within(residual, scale, 1.0e-10)) { return Result::Failure(Failure( domain, "dynamic-initial-equilibrium", "Initial physical acceleration failed the complete free equation.")); } } 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