#include "fesa/elements/hex8_element.h" #include "fesa/math/vector3.h" #include #include #include #include #include #include "../math/dense_blas_internal.h" #include "hex8_geometry.h" #include "hex8_linear_tangent_cache.h" namespace fesa { namespace { using hex8_internal::Tensor; using hex8_internal::Triple; /// @brief Checks every tensor component before recovery or inverse use. bool Finite(const Tensor& value) { for (const auto& row : value) for (double entry : row) if (!std::isfinite(entry)) return false; return true; } /// @brief Packs actual tensor shear, never engineering shear, in schema order. std::array Pack(const Tensor& value) { return {value[0][0], value[1][1], value[2][2], value[0][1], value[0][2], value[1][2]}; } /// @brief Computes a Euclidean contraction in deterministic component order. double Dot(const Triple& a, const Triple& b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } /// @brief Forms a scaled 24-component outer product through dense MKL. Matrix Outer(const std::array& values, double coefficient) { Matrix left{24U, 1U}; Matrix right{1U, 24U}; for (std::size_t a = 0; a < 8; ++a) for (std::size_t i = 0; i < 3; ++i) { left(3U * a + i, 0) = coefficient * values[a][i]; right(0, 3U * a + i) = values[a][i]; } return left.Multiply(right); } /// @brief Uses MKL entry products and the original ordered Gram/stress sums. Matrix GradientProduct(const std::array& gradients, const Tensor* stress = nullptr) { const std::size_t terms = stress == nullptr ? 3U : 9U; Vector left{64U * terms}; Vector right{64U * terms}; Vector final_factor{64U * terms}; for (std::size_t a = 0; a < 8; ++a) for (std::size_t b = 0; b < 8; ++b) for (std::size_t j = 0; j < 3; ++j) for (std::size_t l = 0; l < (stress == nullptr ? 1U : 3U); ++l) { const auto entry = (8U * a + b) * terms + (stress == nullptr ? j : 3U * j + l); left[entry] = gradients[a][j]; right[entry] = stress == nullptr ? gradients[b][j] : (*stress)[l][j]; final_factor[entry] = gradients[b][l]; } auto products = dense_blas_internal::MultiplyDiagonal(left, right); if (stress != nullptr) products = dense_blas_internal::MultiplyDiagonal(products, final_factor); Matrix result{8U, 8U}; for (std::size_t a = 0; a < 8; ++a) for (std::size_t b = 0; b < 8; ++b) { const auto first = (8U * a + b) * terms; if (stress == nullptr) { result(a, b) = (products[first] + products[first + 1U]) + products[first + 2U]; } else { for (std::size_t term = 0; term < terms; ++term) result(a, b) += products[first + term]; } } return result; } /// @brief Retains product and cancellation errors in fixed-order contractions. void AccumulateProduct(double left, double right, double& sum, double& correction) { const double product = left * right; const double next = sum + product; correction += std::fma(left, right, -product) + (std::abs(sum) >= std::abs(product) ? (sum - next) + product : (product - next) + sum); sum = next; } /// @brief Recovers low bits lost when subtracting unequal nodal translations. double DifferenceCorrection(double left, double right, double difference) { const double right_virtual = left - difference; const double left_virtual = difference + right_virtual; return (left - left_virtual) + (right_virtual - right); } /// @brief Gives the finite-control failure without accepting a geometry guess. const char* CertificateMessage(hex8_internal::Certificate certificate) { switch (certificate) { case hex8_internal::Certificate::kNonpositive: return "Whole-cell Jacobian has a nonpositive witness."; case hex8_internal::Certificate::kUncertain: return "Whole-cell Jacobian positivity is unresolved at depth 10 or cell " "budget 65536."; case hex8_internal::Certificate::kNonfinite: return "Whole-cell Jacobian interval arithmetic is nonfinite."; case hex8_internal::Certificate::kPositive: break; } return "Invalid Jacobian certificate."; } } // namespace Hex8Element::Hex8Element(EntityIndex index, ElementDofLayout layout, Coordinates coordinates, double kappa, double mu, std::optional density, SourceLocation location) : element_index_{index}, layout_{std::move(layout)}, coordinates_{coordinates}, kappa_{kappa}, mu_{mu}, density_{density}, location_{std::move(location)}, paired_linear_tangent_{ std::make_shared()} {} Status Hex8Element::Failure(const char* message) const { return Status::Failure(FailureCategory::kModel, {{Severity::kError, "invalid-hex8", location_, "C3D8", layout_.source_id.instance_name + ":" + layout_.source_id.source_label_text, message}}); } Result Hex8Element::Create(EntityIndex index, ElementDofLayout layout, Coordinates coordinates, const LinearElasticMaterial& material, std::optional density, SourceLocation location) { const double e = material.YoungsModulus(); const double nu = material.PoissonsRatio(); Hex8Element candidate{index, std::move(layout), coordinates, 0.0, 0.0, density, std::move(location)}; const std::vector components{ DofComponent::kUx, DofComponent::kUy, DofComponent::kUz}; const std::set nodes(candidate.layout_.node_indices.begin(), candidate.layout_.node_indices.end()); if (candidate.layout_.node_indices.size() != 8U || nodes.size() != 8U || candidate.layout_.components_per_node != components) return Result::Failure(candidate.Failure( "Hex8 requires eight distinct nodes with exactly UX, UY, UZ.")); if (!std::isfinite(e) || e <= 0.0 || !std::isfinite(nu) || nu <= -1.0 || nu >= 0.5) return Result::Failure( candidate.Failure("Hex8 requires finite E>0 and -1::Failure( candidate.Failure("Hex8 shear and direct bulk moduli must remain " "finite and representably positive.")); if (density && (!std::isfinite(*density) || *density <= 0.0)) return Result::Failure(candidate.Failure( "Hex8 reference density must be finite and positive.")); const auto certificate = hex8_internal::Certify(coordinates); if (certificate != hex8_internal::Certificate::kPositive) return Result::Failure( candidate.Failure(CertificateMessage(certificate))); const double gauss = 1.0 / std::sqrt(3.0); for (std::size_t g = 0; g < 8; ++g) { auto& point = candidate.points_[g]; point.natural = {g % 2U == 0U ? -gauss : gauss, (g / 2U) % 2U == 0U ? -gauss : gauss, g < 4U ? -gauss : gauss}; std::array derivatives{}; hex8_internal::Shape(point.natural, point.shape, derivatives); const auto jacobian = hex8_internal::Jacobian(coordinates, derivatives); point.weight = hex8_internal::Determinant(jacobian); if (!Finite(jacobian) || !std::isfinite(point.weight) || point.weight <= 0.0) return Result::Failure(candidate.Failure( "Reference quadrature Jacobian must be finite and positive.")); const Vector3 c0{jacobian[0][0], jacobian[1][0], jacobian[2][0]}; const Vector3 c1{jacobian[0][1], jacobian[1][1], jacobian[2][1]}; const Vector3 c2{jacobian[0][2], jacobian[1][2], jacobian[2][2]}; if (!c0.Cross(c1).IsFinite() || !c1.Cross(c2).IsFinite() || !c2.Cross(c0).IsFinite()) return Result::Failure( candidate.Failure("Reference Jacobian inverse is nonfinite.")); Matrix coefficients{3U, 3U}; Matrix natural_gradients{3U, 8U}; for (std::size_t i = 0; i < 3U; ++i) { for (std::size_t d = 0; d < 3U; ++d) coefficients(i, d) = jacobian[i][d]; for (std::size_t a = 0; a < 8U; ++a) natural_gradients(i, a) = derivatives[a][i]; } const auto factor = dense_blas_internal::DenseFactor::Lu(coefficients); if (!factor.HasValue()) return Result::Failure( candidate.Failure("Reference Jacobian inverse is nonfinite.")); // The original geometry gate required every reciprocal action to be // finite, including entries not exposed by the smaller shape-gradient RHS. Matrix canonical{3U, 1U}; for (std::size_t axis = 0; axis < 3U; ++axis) { if (axis != 0U) canonical(axis - 1U, 0U) = 0.0; canonical(axis, 0U) = 1.0; if (!factor.Value().Solve(canonical, true).HasValue()) return Result::Failure( candidate.Failure("Reference Jacobian inverse is nonfinite.")); } // J has Cartesian rows and natural columns: J^T grad_x N = grad_nat N. const auto gradients = factor.Value().Solve(natural_gradients, true); if (!gradients.HasValue()) return Result::Failure(candidate.Failure( "Reference gradient or point coordinate is nonfinite.")); point.position = coordinates[0]; for (std::size_t a = 0; a < 8; ++a) for (std::size_t i = 0; i < 3; ++i) { point.position[i] += point.shape[a] * (coordinates[a][i] - coordinates[0][i]); point.gradients[a][i] = gradients.Value()(i, a); if (!std::isfinite(point.gradients[a][i]) || !std::isfinite(point.position[i])) return Result::Failure(candidate.Failure( "Reference gradient or point coordinate is nonfinite.")); } } // Zero Hessian of the same reference-relative mean-volume energy. std::array integrated_gradients{}; double volume = 0.0; bool backend_inputs_failed = false; for (const auto& point : candidate.points_) { auto gradients = point.gradients; gradients[0] = {}; for (std::size_t a = 1; a < 8; ++a) for (std::size_t i = 0; i < 3; ++i) gradients[0][i] -= gradients[a][i]; volume += point.weight; for (std::size_t a = 0; a < 8; ++a) for (std::size_t i = 0; i < 3; ++i) integrated_gradients[a][i] += point.weight * gradients[a][i]; Matrix deviatoric{0U, 0U}; Matrix shear{0U, 0U}; Matrix gram{0U, 0U}; bool point_products_failed = false; try { deviatoric = Outer(gradients, -(2.0 / 3.0) * candidate.mu_); shear = Outer(gradients, candidate.mu_); gram = GradientProduct(gradients); } catch (const std::invalid_argument&) { backend_inputs_failed = true; point_products_failed = true; } for (std::size_t a = 0; a < 8; ++a) for (std::size_t b = 0; b < 8; ++b) { for (std::size_t i = 0; i < 3; ++i) for (std::size_t k = 0; k < 3; ++k) { if (point_products_failed) { // Diagnostic replay only: this candidate can never succeed. const auto& ga = gradients[a]; const auto& gb = gradients[b]; candidate.stiffness_(3U * a + i, 3U * b + k) += point.weight * (-(2.0 / 3.0) * candidate.mu_ * ga[i] * gb[k] + candidate.mu_ * ga[k] * gb[i] + (i == k ? candidate.mu_ * Dot(ga, gb) : 0.0)); } else { candidate.stiffness_(3U * a + i, 3U * b + k) += point.weight * (deviatoric(3U * a + i, 3U * b + k) + shear(3U * a + k, 3U * b + i) + (i == k ? candidate.mu_ * gram(a, b) : 0.0)); } } } } if (!std::isfinite(volume) || volume <= 0.0) return Result::Failure( candidate.Failure("Hex8 reference volume is nonfinite.")); Matrix bulk{0U, 0U}; try { bulk = Outer(integrated_gradients, candidate.kappa_ / volume); } catch (const std::invalid_argument&) { backend_inputs_failed = true; bulk = Matrix{24U, 24U}; for (std::size_t a = 0; a < 8; ++a) for (std::size_t b = 0; b < 8; ++b) for (std::size_t i = 0; i < 3; ++i) for (std::size_t k = 0; k < 3; ++k) bulk(3U * a + i, 3U * b + k) = (candidate.kappa_ / volume) * integrated_gradients[a][i] * integrated_gradients[b][k]; } for (std::size_t i = 0; i < 24; ++i) for (std::size_t j = 0; j < 24; ++j) candidate.stiffness_(i, j) += bulk(i, j); for (std::size_t i = 0; i < 24; ++i) for (std::size_t j = 0; j < 24; ++j) if (!std::isfinite(candidate.stiffness_(i, j))) return Result::Failure( candidate.Failure("Hex8 reference stiffness is nonfinite.")); for (std::size_t i = 0; i < 24; ++i) if (candidate.stiffness_(i, i) <= 0.0) return Result::Failure( candidate.Failure("Hex8 reference stiffness has an unrepresentable " "positive diagonal.")); if (backend_inputs_failed) return Result::Failure(candidate.Failure( "Hex8 dense MKL operands failed finite validation.")); return Result::Success(std::move(candidate)); } const ElementDofLayout& Hex8Element::DofLayout() const noexcept { return layout_; } Result Hex8Element::ComputeStiffness() const { return Result::Success({layout_, stiffness_}); } std::array Hex8Element::ReferenceWeights() const noexcept { std::array weights{}; for (std::size_t g = 0; g < weights.size(); ++g) weights[g] = points_[g].weight; return weights; } Result Hex8Element::ComputeMass() const { if (!density_) return Result::Failure( Failure("Hex8 mass requires reference density.")); Matrix consistent{24U, 24U}; for (const auto& point : points_) { Matrix left{8U, 1U}; Matrix right{1U, 8U}; for (std::size_t a = 0; a < 8; ++a) { left(a, 0) = *density_ * point.shape[a]; right(0, a) = point.shape[a]; } Matrix products{0U, 0U}; try { products = left.Multiply(right); } catch (const std::invalid_argument&) { return Result::Failure( Failure("Hex8 reference mass is nonfinite.")); } for (std::size_t a = 0; a < 8; ++a) for (std::size_t b = 0; b < 8; ++b) { const double value = products(a, b) * point.weight; if (!std::isfinite(value)) return Result::Failure( Failure("Hex8 reference mass is nonfinite.")); for (std::size_t i = 0; i < 3; ++i) consistent(3U * a + i, 3U * b + i) += value; } } Matrix mass{24U, 24U}; for (std::size_t i = 0; i < 24; ++i) for (std::size_t j = 0; j < 24; ++j) { if (!std::isfinite(consistent(i, j))) return Result::Failure( Failure("Hex8 accumulated reference mass is nonfinite.")); mass(i, i) += consistent(i, j); } for (std::size_t i = 0; i < 24; ++i) if (!std::isfinite(mass(i, i)) || mass(i, i) <= 0.0) return Result::Failure(Failure( "Hex8 reference mass has an unrepresentable positive diagonal.")); return Result::Success( {layout_, std::move(mass)}); } Result Hex8Element::Recover(const Vector& u) const { auto response = EvaluateLinear(u); if (!response.HasValue()) return Result::Failure(response.GetStatus()); SolidElementResultRows rows; rows.rows.assign(response.Value().points.begin(), response.Value().points.end()); rows.physical_strain_energy = response.Value().strain_energy; return Result::Success( {layout_.source_id, std::move(rows)}); } Result Hex8Element::EvaluateLinear(const Vector& u) const { return Evaluate(u, false); } Result Hex8Element::EvaluateNonlinear(const Vector& u) const { return Evaluate(u, true); } Result Hex8Element::EvaluateLinearHighLow( const Vector& high, const Vector& low) const { return EvaluateHighLow(high, low, false); } Result Hex8Element::EvaluateNonlinearHighLow( const Vector& high, const Vector& low) const { return EvaluateHighLow(high, low, true); } Result Hex8Element::Evaluate(const Vector& u, bool nonlinear) const { if (u.Size() != 24U) return Result::Failure( Failure("Hex8 trial requires 24 translations.")); const auto certificate = hex8_internal::Certify(coordinates_, &u); if (certificate != hex8_internal::Certificate::kPositive) return Result::Failure( Failure(CertificateMessage(certificate))); Hex8Response response; Vector relative{24U}; Vector relative_correction{24U}; for (std::size_t a = 0; a < 8; ++a) for (std::size_t i = 0; i < 3; ++i) { const auto row = 3U * a + i; relative[row] = u[row] - u[i]; relative_correction[row] = DifferenceCorrection(u[row], u[i], relative[row]); } if (!nonlinear) { response.material_tangent = stiffness_; response.tangent = stiffness_; } std::array deformations{}, strains{}; std::array, 8> gradients{}; double volume = 0.0, trace_integral = 0.0; Vector force_correction{24U}; for (std::size_t g = 0; g < 8; ++g) { const auto& point = points_[g]; gradients[g] = point.gradients; Triple sum{}; for (std::size_t a = 1; a < 8; ++a) for (std::size_t j = 0; j < 3; ++j) sum[j] += gradients[g][a][j]; for (std::size_t j = 0; j < 3; ++j) gradients[g][0][j] = -sum[j]; Tensor gradient{}; Tensor gradient_correction{}; for (std::size_t a = 1; a < 8; ++a) for (std::size_t i = 0; i < 3; ++i) for (std::size_t j = 0; j < 3; ++j) { AccumulateProduct(relative[3U * a + i], point.gradients[a][j], gradient[i][j], gradient_correction[i][j]); AccumulateProduct(relative_correction[3U * a + i], point.gradients[a][j], gradient[i][j], gradient_correction[i][j]); } for (std::size_t i = 0; i < 3; ++i) for (std::size_t j = 0; j < 3; ++j) gradient[i][j] += gradient_correction[i][j]; Tensor f = nonlinear ? gradient : Tensor{}; for (std::size_t i = 0; i < 3; ++i) f[i][i] += 1.0; Tensor strain{}; for (std::size_t i = 0; i < 3; ++i) for (std::size_t j = 0; j < 3; ++j) { strain[i][j] = 0.5 * (gradient[i][j] + gradient[j][i]); // E = sym(H) + H^T*H/2 avoids subtracting I at tiny strain. if (nonlinear) for (std::size_t k = 0; k < 3; ++k) strain[i][j] += 0.5 * gradient[k][i] * gradient[k][j]; } deformations[g] = f; strains[g] = strain; volume += point.weight; trace_integral += point.weight * (strain[0][0] + strain[1][1] + strain[2][2]); if (!Finite(gradient) || !Finite(f) || !Finite(strain)) return Result::Failure( Failure("Hex8 trial tensor is nonfinite.")); } const double mean_trace = trace_integral / volume; if (!std::isfinite(volume) || volume <= 0 || !std::isfinite(mean_trace)) return Result::Failure( Failure("Hex8 mean volume is nonfinite.")); response.strain_energy = (0.5 * kappa_ * mean_trace) * (volume * mean_trace); std::array integrated_fg{}; bool backend_inputs_failed = false; for (std::size_t g = 0; g < 8; ++g) { const auto& point = points_[g]; const auto& f = deformations[g]; const auto& strain = strains[g]; const double trace = strain[0][0] + strain[1][1] + strain[2][2]; Tensor s{}, p{}, fft{}; double density_energy = 0.0; for (std::size_t i = 0; i < 3; ++i) for (std::size_t j = 0; j < 3; ++j) { const double dev = strain[i][j] - (i == j ? trace / 3.0 : 0.0); s[i][j] = 2.0 * mu_ * dev + (i == j ? kappa_ * mean_trace : 0.0); density_energy += mu_ * dev * dev; } for (std::size_t i = 0; i < 3; ++i) for (std::size_t j = 0; j < 3; ++j) { for (std::size_t k = 0; k < 3; ++k) { p[i][j] += f[i][k] * s[k][j]; fft[i][j] += f[i][k] * f[j][k]; } } response.strain_energy += density_energy * point.weight; Tensor stress = s; if (nonlinear) { const double det_f = hex8_internal::Determinant(f); if (!std::isfinite(det_f) || det_f <= 0.0) return Result::Failure(Failure( "Hex8 trial deformation determinant must be finite and positive.")); stress = {}; for (std::size_t i = 0; i < 3; ++i) for (std::size_t j = 0; j < 3; ++j) for (std::size_t k = 0; k < 3; ++k) stress[i][j] += p[i][k] * f[j][k] / det_f; } for (std::size_t a = 0; a < 8; ++a) { for (std::size_t i = 0; i < 3; ++i) integrated_fg[a][i] += point.weight * Dot(f[i], gradients[g][a]); } Matrix deviatoric{0U, 0U}; Matrix shear{0U, 0U}; Matrix gram{0U, 0U}; Matrix geometric{0U, 0U}; bool point_products_failed = false; if (!Finite(f) || !Finite(strain) || !Finite(s) || !Finite(p) || !Finite(fft) || !Finite(stress)) return Result::Failure( Failure("Hex8 trial tensor is nonfinite.")); if (nonlinear) { std::array fg{}; for (std::size_t a = 0; a < 8; ++a) for (std::size_t i = 0; i < 3; ++i) fg[a][i] = Dot(f[i], gradients[g][a]); try { deviatoric = Outer(fg, -(2.0 / 3.0) * mu_); shear = Outer(fg, mu_); gram = GradientProduct(gradients[g]); geometric = GradientProduct(gradients[g], &s); } catch (const std::invalid_argument&) { backend_inputs_failed = true; point_products_failed = true; } } for (std::size_t a = 0; a < 8; ++a) { const auto& ga = gradients[g][a]; for (std::size_t i = 0; i < 3; ++i) { double contraction = 0.0; double contraction_correction = 0.0; for (std::size_t j = 0; j < 3; ++j) AccumulateProduct(p[i][j], ga[j], contraction, contraction_correction); AccumulateProduct(contraction, point.weight, response.internal_force[3U * a + i], force_correction[3U * a + i]); AccumulateProduct(contraction_correction, point.weight, response.internal_force[3U * a + i], force_correction[3U * a + i]); } if (nonlinear) for (std::size_t b = 0; b < 8; ++b) { const auto& gb = gradients[g][b]; double diagnostic_geometric = 0.0; if (point_products_failed) for (std::size_t j = 0; j < 3; ++j) for (std::size_t l = 0; l < 3; ++l) diagnostic_geometric += ga[j] * s[l][j] * gb[l]; for (std::size_t i = 0; i < 3; ++i) for (std::size_t k = 0; k < 3; ++k) { const auto row = 3U * a + i; const auto column = 3U * b + k; if (point_products_failed) { // Reproduce original diagnostic priority, never acceptance. response.material_tangent(row, column) += point.weight * (-(2.0 / 3.0) * mu_ * Dot(f[i], ga) * Dot(f[k], gb) + mu_ * Dot(f[i], gb) * Dot(f[k], ga) + mu_ * fft[i][k] * Dot(ga, gb)); } else { response.material_tangent(row, column) += point.weight * (deviatoric(row, column) + shear(3U * b + i, 3U * a + k) + mu_ * fft[i][k] * gram(a, b)); } if (i == k) response.geometric_tangent(row, column) += (point_products_failed ? diagnostic_geometric : geometric(a, b)) * point.weight; } } } response.points[g] = {element_index_, static_cast(g + 1U), point.natural, point.position, Pack(stress), Pack(strain)}; } for (std::size_t i = 0; i < 24; ++i) response.internal_force[i] += force_correction[i]; if (nonlinear) { Matrix bulk{0U, 0U}; try { bulk = Outer(integrated_fg, kappa_ / volume); } catch (const std::invalid_argument&) { backend_inputs_failed = true; bulk = Matrix{24U, 24U}; for (std::size_t a = 0; a < 8; ++a) for (std::size_t b = 0; b < 8; ++b) for (std::size_t i = 0; i < 3; ++i) for (std::size_t k = 0; k < 3; ++k) bulk(3U * a + i, 3U * b + k) = (kappa_ / volume) * integrated_fg[a][i] * integrated_fg[b][k]; } for (std::size_t i = 0; i < 24; ++i) for (std::size_t j = 0; j < 24; ++j) response.material_tangent(i, j) += bulk(i, j); } if (!std::isfinite(response.strain_energy)) return Result::Failure( Failure("Hex8 trial energy is nonfinite.")); for (std::size_t i = 0; i < 24; ++i) { if (!std::isfinite(response.internal_force[i])) return Result::Failure( Failure("Hex8 trial force is nonfinite.")); for (std::size_t j = 0; j < 24; ++j) { if (nonlinear) response.tangent(i, j) = response.material_tangent(i, j) + response.geometric_tangent(i, j); if (!std::isfinite(response.tangent(i, j)) || !std::isfinite(response.material_tangent(i, j)) || !std::isfinite(response.geometric_tangent(i, j))) return Result::Failure( Failure("Hex8 trial tangent is nonfinite.")); } } if (backend_inputs_failed) return Result::Failure( Failure("Hex8 dense MKL operands failed finite validation.")); return Result::Success(std::move(response)); } } // namespace fesa