Merge reviewed MKL shell products and complete-trial reuse

This commit is contained in:
KOKO\Mimi
2026-10-14 21:49:14 +09:00
14 changed files with 1936 additions and 219 deletions
+143 -49
View File
@@ -2,9 +2,11 @@
#include <array>
#include <cmath>
#include <cstddef>
#include <exception>
#include <iomanip>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
@@ -13,6 +15,7 @@
#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"
namespace fesa {
namespace hht_internal {
@@ -132,13 +135,13 @@ 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<std::array<double, 6>, 6> block{};
std::array<std::array<double, 6>, 6> lower{};
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]));
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)) {
@@ -149,36 +152,34 @@ Status CheckLocalMass(const Domain& domain, const SparseMatrix& matrix,
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]);
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];
}
}
Matrix lower{size, size};
Matrix inverse_lower{size, size};
auto factor_failure = dense_blas_internal::CholeskyFailure::kNone;
try {
if (!dense_blas_internal::CholeskyLowerAndInverse(
block, lower, inverse_lower, &factor_failure)) {
if (factor_failure == dense_blas_internal::CholeskyFailure::kInverse)
return Failure(
domain, "dynamic-positive-mass-conditioning",
"A nodal free physical block reciprocal condition is below 2^-32.");
return Failure(
domain, "dynamic-positive-mass",
"Physical mass requires strictly positive Cholesky pivots.");
}
} catch (const std::exception& error) {
return Failure(domain, "dynamic-positive-mass", error.what());
}
const Matrix inverse =
dense_blas_internal::MultiplyTransposeLeft(inverse_lower, inverse_lower);
std::array<double, 6> sums{};
for (std::size_t column = 0; column < size; ++column) {
std::array<double, 6> 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]);
sums[i] += std::abs(inverse(i, column));
}
}
const double inverse_norm = *std::max_element(sums.begin(), sums.end());
@@ -212,19 +213,33 @@ Result<PhysicalSpace> BuildPhysicalSpace(
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<double> 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;
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]] +=
entry * assembly.rotations[full / 6U].drill_null[full % 6U - 3U];
action[node_null[full / 6U]] += null_products[index];
}
}
for (const double value : action)
@@ -291,6 +306,7 @@ Result<PhysicalSpace> BuildPhysicalSpace(
}
}
std::vector<CooContribution> entries;
std::vector<std::pair<double, double>> mass_factors;
node_starts.push_back(dimension);
double complement_gram_error = 0.0;
double null_gram_error = 0.0;
@@ -350,8 +366,10 @@ Result<PhysicalSpace> BuildPhysicalSpace(
domain, "dynamic-invalid-analytic-null",
"The scaled physical and analytic null Gram identities failed."));
}
for (std::size_t i = 0; i < dimension; ++i)
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];
@@ -359,13 +377,54 @@ Result<PhysicalSpace> BuildPhysicalSpace(
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()});
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<std::size_t> 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<PhysicalSpace>::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<PhysicalSpace>::Failure(block.GetStatus());
@@ -436,15 +495,8 @@ Result<Regularity> CheckRegularity(const Domain& domain,
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];
}
}
const double stiffness = OrderedNullMetric(
assembly.drilling_metric, nodes[i], n);
if (!std::isfinite(stiffness) || !(stiffness > 0.0)) {
return Result<Regularity>::Failure(
Failure(domain, "dynamic-algebraic-regularity",
@@ -454,22 +506,37 @@ Result<Regularity> CheckRegularity(const Domain& domain,
weights[i] = std::sqrt(stiffness);
}
std::vector<CooContribution> entries;
std::vector<double> 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] *
assembly.rotations[full / 6U].drill_null[full % 6U - 3U] /
weights[row] / weights[column],
0U, entries.size()});
{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<Regularity>::Failure(matrix.GetStatus());
@@ -595,6 +662,33 @@ Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
} // namespace
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<double>::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<double>::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<std::size_t> NullNodes(
const DofManager& dofs,
const std::vector<DynamicRotationKinematics>& rotations) {
+123 -4
View File
@@ -1,4 +1,5 @@
#include <algorithm>
#include <cfenv>
#include <cmath>
#include <limits>
#include <map>
@@ -6,13 +7,41 @@
#include <string>
#include <utility>
#if defined(_M_X64)
#include <xmmintrin.h>
#endif
#include "analysis/hht_dynamic_internal.h"
#include "math/dense_blas_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 {
TrialEvaluationContext ReadTrialEvaluationContext() noexcept {
TrialEvaluationContext result{};
#if defined(_M_X64)
result.rounding = std::fegetround();
result.mxcsr_control = _mm_getcsr() & 0xFFC0U;
result.dense_policy = dense_blas_internal::ReadScopedLocalDensePolicy();
result.supported = result.rounding >= 0;
#endif
return result;
}
bool ReusableTrialContext(const TrialEvaluationContext& before,
const TrialEvaluationContext& after) noexcept {
return before.supported && after.supported &&
before.rounding == after.rounding &&
before.mxcsr_control == after.mxcsr_control &&
(before.mxcsr_control & 0x1F80U) == 0x1F80U &&
before.dense_policy.blas_max_threads == 1 &&
after.dense_policy.blas_max_threads == 1 &&
before.dense_policy.dynamic == 0 && after.dense_policy.dynamic == 0;
}
namespace {
/// @brief Reports a dynamic assembly or equilibrium failure.
@@ -54,6 +83,27 @@ Vector Gather(const Vector& full, const RuntimeShell& shell) {
return local;
}
/// @brief Preserves rejected entries while batching finite products in MKL.
Vector EntryProducts(const Vector& left, const Vector& right) {
Vector result(left.Size(), std::numeric_limits<double>::quiet_NaN());
std::vector<std::size_t> positions;
positions.reserve(left.Size());
for (std::size_t i = 0U; i < left.Size(); ++i)
if (std::isfinite(left[i]) && std::isfinite(right[i])) positions.push_back(i);
Vector valid_left{positions.size()};
Vector valid_right{positions.size()};
for (std::size_t i = 0U; i < positions.size(); ++i) {
valid_left[i] = left[positions[i]];
valid_right[i] = right[positions[i]];
}
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
const Vector products =
dense_blas_internal::MultiplyDiagonal(valid_left, valid_right);
for (std::size_t i = 0U; i < positions.size(); ++i)
result[positions[i]] = products[i];
return result;
}
} // namespace
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
@@ -366,6 +416,10 @@ Result<DynamicAssembly> EvaluateAssembly(
std::vector<std::optional<NonlinearMitc4InertiaEvaluation>> inertia(
shells.size());
std::vector<std::optional<Status>> failures(shells.size());
const bool retain_constraints =
directional_third && !frame.is_initial && std::isfinite(frame.time);
std::vector<TrialEvaluationContext> evaluation_contexts(
retain_constraints ? shells.size() : 0U);
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]);
@@ -375,12 +429,18 @@ Result<DynamicAssembly> EvaluateAssembly(
trial.displacement[node] = Triple(q, 6U * node);
trial.rotation_vector[node] = Triple(q, 6U * node + 3U);
}
const auto before = retain_constraints ? ReadTrialEvaluationContext()
: TrialEvaluationContext{};
auto f = EvaluateInternalResidualAndTangent(
shells[e].internal, trial, directional_third ? &v : nullptr);
if (!f.HasValue()) {
failures[e] = f.GetStatus();
return;
}
if (retain_constraints) {
const auto after = ReadTrialEvaluationContext();
if (ReusableTrialContext(before, after)) evaluation_contexts[e] = after;
}
auto m = shells[e].inertia.Evaluate(trial, v, a);
if (!m.HasValue()) {
failures[e] = m.GetStatus();
@@ -455,6 +515,15 @@ Result<DynamicAssembly> EvaluateAssembly(
}
}
const auto a = Gather(frame.acceleration, shells[e]);
Vector mass_values{24U * 24U};
Vector acceleration_values{24U * 24U};
for (std::size_t row = 0U; row < 24U; ++row)
for (std::size_t column = 0U; column < 24U; ++column) {
const auto index = 24U * row + column;
mass_values[index] = m.mass(row, column);
acceleration_values[index] = a[column];
}
const Vector mass_action = EntryProducts(mass_values, acceleration_values);
for (std::size_t row = 0U; row < 24U; ++row) {
const auto global = shells[e].scatter[row];
frame.internal_force[global] += f.internal_force[row];
@@ -466,7 +535,7 @@ Result<DynamicAssembly> EvaluateAssembly(
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]);
std::abs(mass_action[24U * row + column]);
const std::array<double, 6> values{
m.mass(row, column), m.position_tangent(row, column),
m.velocity_tangent(row, column), f.internal_tangent(row, column),
@@ -543,9 +612,20 @@ Result<DynamicAssembly> EvaluateAssembly(
Vector hidden_second(domain.Nodes().size());
Vector hidden_second_scale(domain.Nodes().size());
for (std::size_t node = 0U; node < constraint_hessians.size(); ++node) {
Vector coefficients{constraint_hessians[node].size()};
Vector first_velocity{coefficients.Size()};
Vector second_velocity{coefficients.Size()};
std::size_t index = 0U;
for (const auto& entry : constraint_hessians[node]) {
const double term = entry.second * frame.velocity[entry.first.first] *
frame.velocity[entry.first.second];
coefficients[index] = entry.second;
first_velocity[index] = frame.velocity[entry.first.first];
second_velocity[index] = frame.velocity[entry.first.second];
++index;
}
const Vector products = EntryProducts(
EntryProducts(coefficients, first_velocity), second_velocity);
for (std::size_t i = 0U; i < products.Size(); ++i) {
const double term = products[i];
hidden_second[node] += term;
hidden_second_scale[node] += std::abs(term);
}
@@ -563,13 +643,52 @@ Result<DynamicAssembly> EvaluateAssembly(
return Result<DynamicAssembly>::Failure(gradient.GetStatus());
hidden_gradient = std::move(gradient.Value());
}
std::optional<TrialConstraintData> trial_constraints;
if (retain_constraints) {
TrialConstraintData data{};
data.source_path = domain.SourcePath();
data.time = frame.time;
data.frame_index = frame.frame_index;
data.increment_index = frame.increment_index;
data.iteration_count = frame.iteration_count;
data.shells.resize(shells.size());
for (std::size_t e = 0U; e < shells.size(); ++e) {
if (!ReusableTrialContext(evaluation_contexts[e], evaluation_contexts[e]))
continue;
auto& record = data.shells[e];
record.element_index = shells[e].element_index;
record.scatter = shells[e].scatter;
record.configuration =
nonlinear_mitc4_internal::ConfigurationSnapshot::Read(
shells[e].internal);
record.evaluation_context = evaluation_contexts[e];
for (std::size_t i = 0U; i < 24U; ++i) {
record.q[i] = frame.generalized_coordinates[shells[e].scatter[i]];
record.v[i] = frame.velocity[shells[e].scatter[i]];
}
for (std::size_t node = 0U; node < 4U; ++node)
record.node_source_ids[node] =
domain.Nodes()[shells[e].scatter[6U * node] / 6U].source_id;
auto& response = *internal[e];
record.drilling_constraint = std::move(response.drilling_constraint);
record.drilling_constraint_gradient =
std::move(response.drilling_constraint_gradient);
record.drilling_constraint_hessian =
std::move(response.drilling_constraint_hessian);
record.drilling_constraint_third_directional_gradient =
std::move(response.drilling_constraint_third_directional_gradient);
record.directional_internal = true;
}
trial_constraints = std::move(data);
}
return Result<DynamicAssembly>::Success(
{std::move(frame), std::move(mass.Value()), std::move(position.Value()),
std::move(velocity.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), std::move(hidden_gradient)});
std::move(hidden_second_scale), std::move(hidden_gradient),
std::move(trial_constraints)});
}
Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
+53
View File
@@ -2,6 +2,8 @@
#define FESA_ANALYSIS_HHT_DYNAMIC_INTERNAL_H_
#include <array>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <vector>
@@ -10,6 +12,8 @@
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include "fesa/math/sparse_matrix.h"
#include "elements/nonlinear_mitc4_configuration_internal.h"
#include "math/dense_blas_internal.h"
namespace fesa {
class LinearSolver;
@@ -25,6 +29,49 @@ struct RuntimeShell {
NonlinearMitc4Inertia inertia;
};
/// @brief Captures only controls used by the current MSVC x64 local evaluator.
struct TrialEvaluationContext {
int rounding{-1};
unsigned int mxcsr_control{0U};
dense_blas_internal::LocalDensePolicy dense_policy{0, -1};
bool supported{false};
};
/// @brief Queries metadata without changing FP controls or the restored TLS.
TrialEvaluationContext ReadTrialEvaluationContext() noexcept;
/// @brief Allows only masked, stable, single-worker nondynamic local policy.
bool ReusableTrialContext(const TrialEvaluationContext& before,
const TrialEvaluationContext& after) noexcept;
/// @brief Owns only complete directional constraint data for one local trial.
struct DirectionalShellConstraintData {
EntityIndex element_index{0U};
std::array<std::size_t, 24> scatter{};
std::array<SourceEntityId, 4> node_source_ids{};
nonlinear_mitc4_internal::ConfigurationSnapshot configuration;
std::array<double, 24> q{};
std::array<double, 24> v{};
TrialEvaluationContext evaluation_context;
bool directional_internal{false};
Vector drilling_constraint{0U};
Matrix drilling_constraint_gradient{0U, 0U};
std::array<Matrix, 4> drilling_constraint_hessian{
Matrix{0U, 0U}, Matrix{0U, 0U}, Matrix{0U, 0U}, Matrix{0U, 0U}};
std::array<Vector, 4> drilling_constraint_third_directional_gradient{
Vector{0U}, Vector{0U}, Vector{0U}, Vector{0U}};
};
/// @brief Travels with one complete assembly, never accepted frame/history.
struct TrialConstraintData {
std::filesystem::path source_path;
double time{0.0};
std::uint32_t frame_index{0U};
std::uint32_t increment_index{0U};
std::uint32_t iteration_count{0U};
std::vector<DirectionalShellConstraintData> shells;
};
/// @brief Stores a complete trial with independently assembled operators.
struct DynamicAssembly {
DynamicAcceptedFrameState frame;
@@ -43,6 +90,7 @@ struct DynamicAssembly {
Vector hidden_second_scale{0U};
// Canonical scalar drilling-constraint derivative, indexed by global node.
std::optional<SparseMatrix> hidden_gradient;
std::optional<TrialConstraintData> trial_constraints;
};
/// @brief Builds checked shell evaluators in stable source order.
@@ -95,6 +143,11 @@ std::vector<std::size_t> NullNodes(
const DofManager& dofs,
const std::vector<DynamicRotationKinematics>& rotations);
/// @brief Contracts a nodal metric with original left-associated products.
/// @note Nonfinite products remain for the caller's existing validity gate.
double OrderedNullMetric(const SparseMatrix& metric, std::size_t node,
const Vector3& direction);
/// @brief Solves the positive-mass and differentiated-null initial equations.
///
/// The returned candidate is finite and exact-zero at constrained DOFs. Its
+139 -36
View File
@@ -6,10 +6,12 @@
#include <limits>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include "fesa/solvers/linear/tangent_solver.h"
#include "math/dense_blas_internal.h"
#include "math/high_low_arithmetic.h"
namespace fesa::hht_reduced_internal {
@@ -58,6 +60,88 @@ Vector Local(const Vector& full,
return local;
}
/// @brief Preserves the exact floating representation, including signed zero.
bool SameBits(const double left, const double right) {
return std::memcmp(&left, &right, sizeof(double)) == 0;
}
/// @brief Compares semantic and original source identity without object keys.
bool SameSource(const SourceEntityId& left, const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
/// @brief Finds only the current complete directional response, otherwise miss.
const hht_internal::DirectionalShellConstraintData* MatchingConstraints(
const Domain& domain, const hht_internal::RuntimeShell& shell,
const hht_internal::DynamicAssembly& assembly, const std::size_t index,
const std::size_t shell_count, const Vector& q, const Vector& v) {
if (assembly.frame.is_initial || !assembly.trial_constraints.has_value())
return nullptr;
const auto& owner = *assembly.trial_constraints;
const auto& frame = assembly.frame;
if (owner.shells.size() != shell_count || index >= owner.shells.size() ||
owner.source_path != domain.SourcePath() ||
!std::isfinite(owner.time) || !std::isfinite(frame.time) ||
!SameBits(owner.time, frame.time) ||
owner.frame_index != frame.frame_index ||
owner.increment_index != frame.increment_index ||
owner.iteration_count != frame.iteration_count)
return nullptr;
const auto& record = owner.shells[index];
if (!record.directional_internal ||
record.element_index != shell.element_index ||
record.scatter != shell.scatter ||
!hht_internal::ReusableTrialContext(
record.evaluation_context,
hht_internal::ReadTrialEvaluationContext()))
return nullptr;
const auto configuration =
nonlinear_mitc4_internal::ConfigurationSnapshot::Read(shell.internal);
if (record.configuration.source.file != configuration.source.file ||
record.configuration.source.line != configuration.source.line ||
record.configuration.identity != configuration.identity)
return nullptr;
for (std::size_t i = 0U; i < configuration.values.size(); ++i)
if (!std::isfinite(record.configuration.values[i]) ||
!std::isfinite(configuration.values[i]) ||
!SameBits(record.configuration.values[i], configuration.values[i]))
return nullptr;
for (std::size_t i = 0U; i < 24U; ++i)
if (!std::isfinite(record.q[i]) || !std::isfinite(q[i]) ||
!std::isfinite(record.v[i]) || !std::isfinite(v[i]) ||
!SameBits(record.q[i], q[i]) || !SameBits(record.v[i], v[i]))
return nullptr;
for (std::size_t node = 0U; node < 4U; ++node) {
const auto global_node = shell.scatter[6U * node] / 6U;
if (global_node >= domain.Nodes().size() ||
!SameSource(record.node_source_ids[node],
domain.Nodes()[global_node].source_id))
return nullptr;
}
if (record.drilling_constraint.Size() != 4U ||
record.drilling_constraint_gradient.Rows() != 4U ||
record.drilling_constraint_gradient.Columns() != 24U)
return nullptr;
for (std::size_t node = 0U; node < 4U; ++node) {
const auto& hessian = record.drilling_constraint_hessian[node];
const auto& third =
record.drilling_constraint_third_directional_gradient[node];
if (hessian.Rows() != 24U || hessian.Columns() != 24U ||
third.Size() != 24U || !std::isfinite(record.drilling_constraint[node]))
return nullptr;
for (std::size_t i = 0U; i < 24U; ++i) {
if (!std::isfinite(record.drilling_constraint_gradient(node, i)) ||
!std::isfinite(third[i]))
return nullptr;
for (std::size_t j = 0U; j < 24U; ++j)
if (!std::isfinite(hessian(i, j))) return nullptr;
}
}
return &record;
}
NonlinearMitc4TrialDofs Trial(const Vector& local) {
NonlinearMitc4TrialDofs trial{};
for (std::size_t node = 0U; node < 4U; ++node) {
@@ -85,19 +169,6 @@ Result<SparseMatrix> DenseSparse(const Matrix& matrix) {
std::move(entries));
}
double SparseEntry(const SparseMatrix& matrix, const std::size_t row,
const std::size_t column) {
const auto first = matrix.RowOffsets()[row];
const auto last = matrix.RowOffsets()[row + 1U];
const auto found = std::lower_bound(matrix.ColumnIndices().begin() + first,
matrix.ColumnIndices().begin() + last,
column);
if (found == matrix.ColumnIndices().begin() + last || *found != column)
return 0.0;
return matrix.Values()[static_cast<std::size_t>(
found - matrix.ColumnIndices().begin())];
}
bool ValidBasis(const FixedChart& chart) {
if (chart.p_columns.size() != chart.p_dofs.size() ||
chart.z_columns.size() != chart.z_dofs.size()) return false;
@@ -454,32 +525,46 @@ Result<ConstraintOperators> EvaluateConstraintOperators(
node_row[chart.null_nodes[row]] = row;
}
std::vector<bool> external_done(domain.Nodes().size(), false);
std::size_t shell_index = 0U;
for (const auto& shell : shells) {
const auto q = Local(assembly.frame.generalized_coordinates, shell);
const auto v = Local(assembly.frame.velocity, shell);
const auto internal = EvaluateInternalResidualAndTangent(shell.internal,
Trial(q), &v);
if (!internal.HasValue())
return Result<ConstraintOperators>::Failure(internal.GetStatus());
const auto* saved = MatchingConstraints(domain, shell, assembly,
shell_index++, shells.size(), q, v);
std::optional<NonlinearMitc4InternalEquilibrium> fresh;
if (saved == nullptr) {
auto internal =
EvaluateInternalResidualAndTangent(shell.internal, Trial(q), &v);
if (!internal.HasValue())
return Result<ConstraintOperators>::Failure(internal.GetStatus());
fresh = std::move(internal.Value());
}
const auto& constraint = saved != nullptr ? saved->drilling_constraint
: fresh->drilling_constraint;
const auto& gradient_data = saved != nullptr
? saved->drilling_constraint_gradient
: fresh->drilling_constraint_gradient;
const auto& third_data =
saved != nullptr
? saved->drilling_constraint_third_directional_gradient
: fresh->drilling_constraint_third_directional_gradient;
const auto& hessian_data = saved != nullptr
? saved->drilling_constraint_hessian
: fresh->drilling_constraint_hessian;
for (std::size_t local_node = 0U; local_node < 4U; ++local_node) {
const auto global_node = shell.scatter[6U * local_node] / 6U;
if (global_node >= node_row.size() || node_row[global_node] == rows)
continue;
const auto row = node_row[global_node];
result.rows[row].value +=
internal.Value().drilling_constraint[local_node];
result.rows[row].value += constraint[local_node];
for (std::size_t i = 0U; i < 24U; ++i) {
const auto global_i = shell.scatter[i];
const double gradient =
internal.Value().drilling_constraint_gradient(local_node, i);
const double third = internal.Value()
.drilling_constraint_third_directional_gradient
[local_node][i];
const double gradient = gradient_data(local_node, i);
const double third = third_data[local_node][i];
if (gradient != 0.0) gradients[row][global_i] += gradient;
if (third != 0.0) third_gradients[row][global_i] += third;
for (std::size_t j = 0U; j < 24U; ++j) {
const double hessian =
internal.Value().drilling_constraint_hessian[local_node](i, j);
const double hessian = hessian_data[local_node](i, j);
if (hessian != 0.0)
hessians[row][{global_i, shell.scatter[j]}] += hessian;
}
@@ -614,10 +699,33 @@ Matrix PhysicalJacobian(const ConstraintOperators& operators,
Vector ContractConstraintHessian(const ConstraintOperators& operators,
const Vector& left, const Vector& right) {
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
Vector result{operators.rows.size()};
for (std::size_t row = 0U; row < operators.rows.size(); ++row)
for (const auto& entry : operators.rows[row].hessian)
result[row] += entry.value * left[entry.row] * right[entry.column];
for (std::size_t row = 0U; row < operators.rows.size(); ++row) {
const auto& hessian = operators.rows[row].hessian;
Vector coefficients{hessian.size()};
Vector left_values{hessian.size()};
Vector right_values{hessian.size()};
for (std::size_t i = 0U; i < hessian.size(); ++i) {
coefficients[i] = hessian[i].value;
left_values[i] = left[hessian[i].row];
right_values[i] = right[hessian[i].column];
}
try {
const Vector first =
dense_blas_internal::MultiplyDiagonal(coefficients, left_values);
const Vector products =
dense_blas_internal::MultiplyDiagonal(first, right_values);
// Retain the original entry order and left-associated products; only
// ordinary products move to MKL. This is failure-only replay evidence.
for (std::size_t i = 0U; i < products.Size(); ++i)
result[row] += products[i];
} catch (const std::invalid_argument&) {
// A nonfinite input/intermediate cannot yield a finite original sum.
// Keep the existing caller's finite failure without a scalar fallback.
result[row] = std::numeric_limits<double>::quiet_NaN();
}
}
return result;
}
@@ -1186,13 +1294,8 @@ Result<double> CheckGraphRegularity(
Failure("Reduced graph regularity node is invalid.",
"dynamic-mass-regularity"));
const auto& null = assembly.rotations[node].drill_null;
double metric = 0.0;
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j)
metric += null[i] *
SparseEntry(assembly.drilling_metric, 6U * node + 3U + i,
6U * node + 3U + j) *
null[j];
const double metric = hht_internal::OrderedNullMetric(
assembly.drilling_metric, node, null);
if (!std::isfinite(metric) || !(metric > 0.0))
return Result<double>::Failure(Failure(
"Reduced graph requires a positive finite null drilling metric.",
+12 -23
View File
@@ -8,6 +8,7 @@
#include <utility>
#include "fesa/model/model_types.h"
#include "math/dense_blas_internal.h"
namespace fesa {
@@ -223,28 +224,18 @@ Matrix ScaledMatrix(const Matrix& source, double factor) {
return result;
}
/// @brief Lifts a local matrix by a deterministic serial congruence product.
/// @note The loop and accumulation order are part of reproducible stiffness.
/// @brief Lifts a local matrix through MKL and mirrors the upper triangle.
Matrix Congruence(const Matrix& local, const Matrix& transformation) {
if (local.Rows() != local.Columns() ||
local.Rows() != transformation.Rows()) {
throw std::invalid_argument{
"MITC4 congruence dimensions are incompatible."};
}
Matrix result{transformation.Columns(), transformation.Columns()};
Matrix result = dense_blas_internal::MultiplyTransposeLeft(
transformation, local.Multiply(transformation));
for (std::size_t row = 0U; row < result.Rows(); ++row) {
for (std::size_t column = row; column < result.Columns(); ++column) {
double value = 0.0;
for (std::size_t local_row = 0U; local_row < local.Rows(); ++local_row) {
for (std::size_t local_column = 0U; local_column < local.Columns();
++local_column) {
value += transformation(local_row, row) *
local(local_row, local_column) *
transformation(local_column, column);
}
}
result(row, column) = value;
result(column, row) = value;
result(column, row) = result(row, column);
}
}
return result;
@@ -611,7 +602,8 @@ Matrix Mitc4Shell::TransverseShearSectionMatrix() const {
return result;
}
Result<Mitc4Stiffness> Mitc4Shell::Stiffness() const {
Result<Mitc4Stiffness> Mitc4Shell::Stiffness() const try {
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
const Matrix constitutive = MaterialConstitutive5();
const Matrix tying_samples = CovariantTyingShearSamples20();
Matrix physical_local{kPhysicalDofCount, kPhysicalDofCount};
@@ -627,17 +619,12 @@ Result<Mitc4Stiffness> Mitc4Shell::Stiffness() const {
const Matrix strain = StrainDisplacement(
point.natural_coordinates[0], point.natural_coordinates[1],
point.natural_coordinates[2], &tying_samples);
const Matrix integrand = dense_blas_internal::MultiplyTransposeLeft(
strain, constitutive.Multiply(strain));
for (std::size_t row = 0U; row < kPhysicalDofCount; ++row) {
for (std::size_t column = row; column < kPhysicalDofCount; ++column) {
double integrand = 0.0;
for (std::size_t first = 0U; first < 5U; ++first) {
for (std::size_t second = 0U; second < 5U; ++second) {
integrand += strain(first, row) * constitutive(first, second) *
strain(second, column);
}
}
const double contribution =
integrand * geometry.jacobian * point.weight;
integrand(row, column) * geometry.jacobian * point.weight;
physical_local(row, column) += contribution;
if (row != column) {
physical_local(column, row) += contribution;
@@ -714,6 +701,8 @@ Result<Mitc4Stiffness> Mitc4Shell::Stiffness() const {
Mitc4Stiffness{std::move(physical_local), std::move(physical_global),
std::move(drilling_global), std::move(stabilized_global),
drilling_stiffness});
} catch (const std::exception& error) {
return StiffnessFailure(source_location_, identity_, error.what());
}
Result<Mitc4Mass> Mitc4Shell::Mass(const double density) const {
@@ -0,0 +1,24 @@
#ifndef FESA_SRC_FESA_ELEMENTS_NONLINEAR_MITC4_CONFIGURATION_INTERNAL_H_
#define FESA_SRC_FESA_ELEMENTS_NONLINEAR_MITC4_CONFIGURATION_INTERNAL_H_
#include <array>
#include <string>
#include "fesa/elements/nonlinear_mitc4_shell.h"
namespace fesa::nonlinear_mitc4_internal {
/// @brief Owns reference values without depending on an analysis or identity
/// key.
struct ConfigurationSnapshot {
std::array<double, 64> values{};
SourceLocation source{};
std::string identity;
/// @brief Copies all immutable inputs of the internal equilibrium evaluator.
static ConfigurationSnapshot Read(const NonlinearMitc4Shell& element);
};
} // namespace fesa::nonlinear_mitc4_internal
#endif // FESA_SRC_FESA_ELEMENTS_NONLINEAR_MITC4_CONFIGURATION_INTERNAL_H_
+92 -73
View File
@@ -4,11 +4,13 @@
#include <array>
#include <cmath>
#include <cstddef>
#include <exception>
#include <limits>
#include <string>
#include <utility>
#include "fesa/elements/finite_rotation.h"
#include "math/dense_blas_internal.h"
namespace fesa {
namespace {
@@ -35,6 +37,16 @@ class InertiaSum {
double correction_ = 0.0;
};
/// @brief Contracts two nodal 6-by-3 coefficient blocks through dense MKL.
/// @note Nonfinite coefficients remain failures of the final inertia gate.
void DotBlocks(const std::array<double, 18U>& left,
const std::array<double, 18U>& right,
std::array<double, 36U>& result) {
if (!dense_blas_internal::MultiplyTransposeRight(
left.data(), 6U, right.data(), 6U, 3U, result.data()))
result.fill(std::numeric_limits<double>::quiet_NaN());
}
Vector3 WeightedSum(const std::array<double, kNodes>& weights,
const std::array<Vector3, kNodes>& values) {
Vector3 result{};
@@ -442,12 +454,11 @@ Status CheckPhysicalMass(const Matrix& mass,
}
double gram_error = 0.0;
double orthogonality_error = 0.0;
const Matrix gram =
dense_blas_internal::MultiplyTransposeLeft(complement, complement);
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
double entry = row == column ? -1.0 : 0.0;
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
entry += complement(inner, row) * complement(inner, column);
}
const double entry = gram(row, column) - (row == column ? 1.0 : 0.0);
gram_error = std::hypot(gram_error, entry);
}
for (std::size_t node = 0U; node < kNodes; ++node) {
@@ -472,55 +483,42 @@ Status CheckPhysicalMass(const Matrix& mass,
return fail("invalid-nonlinear-inertia-null",
"Scaled mass symmetry and analytic null identities failed.");
}
Matrix block{kPhysical, kPhysical};
Matrix mass_complement{kDofs, kPhysical};
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
mass_complement(row, column) +=
scaled(row, inner) * complement(inner, column);
}
}
}
const Matrix mass_complement = scaled.Multiply(complement);
const Matrix block =
dense_blas_internal::MultiplyTransposeLeft(complement, mass_complement);
double block_norm = 0.0;
for (std::size_t row = 0U; row < kPhysical; ++row) {
double row_sum = 0.0;
for (std::size_t column = 0U; column < kPhysical; ++column) {
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
block(row, column) +=
complement(inner, row) * mass_complement(inner, column);
}
row_sum += std::abs(block(row, column));
}
block_norm = std::max(block_norm, row_sum);
}
Matrix lower{kPhysical, kPhysical};
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column <= row; ++column) {
double entry = block(row, column);
for (std::size_t inner = 0U; inner < column; ++inner) {
entry -= lower(row, inner) * lower(column, inner);
}
if (!std::isfinite(entry) || (row == column && !(entry > 0.0))) {
return fail(
"invalid-nonlinear-inertia-positive-mass",
"Physical mass requires strictly positive Cholesky pivots.");
}
lower(row, column) =
row == column ? std::sqrt(entry) : entry / lower(column, column);
}
Matrix inverse_lower{kPhysical, kPhysical};
auto factor_failure = dense_blas_internal::CholeskyFailure::kNone;
if (!dense_blas_internal::CholeskyLowerAndInverse(block, lower, inverse_lower,
&factor_failure)) {
if (factor_failure == dense_blas_internal::CholeskyFailure::kInverse)
return fail(
"ill-conditioned-nonlinear-inertia-positive-mass",
"Physical mass reciprocal infinity condition is below 2^-32.");
return fail("invalid-nonlinear-inertia-positive-mass",
"Physical mass requires strictly positive Cholesky pivots.");
}
std::array<double, kPhysical> inverse_row_sums{};
double reconstruction_error = 0.0;
double block_frobenius = 0.0;
Matrix transpose{kPhysical, kPhysical};
for (std::size_t row = 0U; row < kPhysical; ++row)
for (std::size_t column = 0U; column < kPhysical; ++column)
transpose(row, column) = lower(column, row);
const Matrix reconstructed = lower.Multiply(transpose);
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
double reconstructed = 0.0;
for (std::size_t inner = 0U; inner <= std::min(row, column); ++inner) {
reconstructed += lower(row, inner) * lower(column, inner);
}
reconstruction_error =
std::hypot(reconstruction_error, reconstructed - block(row, column));
std::hypot(reconstruction_error,
reconstructed(row, column) - block(row, column));
block_frobenius = std::hypot(block_frobenius, block(row, column));
}
}
@@ -528,25 +526,11 @@ Status CheckPhysicalMass(const Matrix& mass,
return fail("invalid-nonlinear-inertia-positive-mass",
"Physical mass Cholesky reconstruction failed.");
}
for (std::size_t column = 0U; column < kPhysical; ++column) {
std::array<double, kPhysical> solution{};
for (std::size_t row = 0U; row < kPhysical; ++row) {
double rhs = row == column ? 1.0 : 0.0;
for (std::size_t inner = 0U; inner < row; ++inner) {
rhs -= lower(row, inner) * solution[inner];
}
solution[row] = rhs / lower(row, row);
}
for (std::size_t reverse = kPhysical; reverse > 0U; --reverse) {
const std::size_t row = reverse - 1U;
double rhs = solution[row];
for (std::size_t inner = row + 1U; inner < kPhysical; ++inner) {
rhs -= lower(inner, row) * solution[inner];
}
solution[row] = rhs / lower(row, row);
inverse_row_sums[row] += std::abs(solution[row]);
}
}
const Matrix inverse =
dense_blas_internal::MultiplyTransposeLeft(inverse_lower, inverse_lower);
for (std::size_t column = 0U; column < kPhysical; ++column)
for (std::size_t row = 0U; row < kPhysical; ++row)
inverse_row_sums[row] += std::abs(inverse(row, column));
const double inverse_norm =
*std::max_element(inverse_row_sums.begin(), inverse_row_sums.end());
const double reciprocal_condition = 1.0 / block_norm / inverse_norm;
@@ -712,6 +696,7 @@ Result<NonlinearMitc4Inertia> NonlinearMitc4Inertia::Create(
Result<NonlinearMitc4InertiaEvaluation> NonlinearMitc4Inertia::Evaluate(
const NonlinearMitc4TrialDofs& trial, const Vector& velocity,
const Vector& acceleration) const {
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
if (velocity.Size() != kDofs || acceleration.Size() != kDofs ||
!IsFinite(velocity) || !IsFinite(acceleration)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
@@ -899,6 +884,42 @@ Result<NonlinearMitc4InertiaEvaluation> NonlinearMitc4Inertia::Evaluate(
result.momentum_rate_scale += scale * acceleration_point.Norm();
result.angular_momentum_rate_scale +=
scale * position.Cross(acceleration_point).Norm();
std::array<double, 18U> h_block;
std::array<double, 18U> velocity_block;
std::array<double, 18U> position_block;
for (std::size_t local = 0U; local < 6U; ++local) {
const auto field = 6U * mass_node + local;
for (std::size_t axis = 0U; axis < 3U; ++axis) {
h_block[3U * local + axis] = h[field][axis];
velocity_block[3U * local + axis] =
velocity_derivative[field][axis];
position_block[3U * local + axis] =
position_derivative[field][axis];
}
}
std::array<double, 36U> mass_block;
std::array<double, 36U> velocity_product;
std::array<double, 36U> position_product;
DotBlocks(h_block, h_block, mass_block);
DotBlocks(h_block, velocity_block, velocity_product);
DotBlocks(h_block, position_block, position_product);
std::array<std::array<double, 36U>, 3U> mass_variation;
for (std::size_t direction = 0U; direction < 3U; ++direction) {
std::array<double, 18U> derivative_block;
for (std::size_t local = 0U; local < 6U; ++local) {
const Vector3 derivative =
local < 3U ? Vector3{}
: z * rotations[mass_node]
.director_second[local - 3U][direction];
for (std::size_t axis = 0U; axis < 3U; ++axis)
derivative_block[3U * local + axis] = derivative[axis];
}
std::array<double, 36U> reverse;
DotBlocks(derivative_block, h_block, mass_variation[direction]);
DotBlocks(h_block, derivative_block, reverse);
for (std::size_t index = 0U; index < 36U; ++index)
mass_variation[direction][index] += reverse[index];
}
for (std::size_t row = 6U * mass_node; row < 6U * (mass_node + 1U);
++row) {
// Keep c independent of a; subtracting M*a destroys small c values.
@@ -906,10 +927,11 @@ Result<NonlinearMitc4InertiaEvaluation> NonlinearMitc4Inertia::Evaluate(
scale * h[row].Dot(convective_point);
for (std::size_t column = 6U * mass_node;
column < 6U * (mass_node + 1U); ++column) {
result.mass(row, column) += scale * h[row].Dot(h[column]);
const auto local_index = 6U * (row % 6U) + column % 6U;
result.mass(row, column) += scale * mass_block[local_index];
result.velocity_tangent(row, column) +=
scale * h[row].Dot(velocity_derivative[column]);
double position_entry = h[row].Dot(position_derivative[column]);
scale * velocity_product[local_index];
double position_entry = position_product[local_index];
if (row / 6U == column / 6U && row % 6U >= 3U &&
column % 6U >= 3U) {
const std::size_t node = row / 6U;
@@ -922,20 +944,10 @@ Result<NonlinearMitc4InertiaEvaluation> NonlinearMitc4Inertia::Evaluate(
result.position_tangent(row, column) += scale * position_entry;
if (column % 6U >= 3U) {
const auto direction = column % 6U - 3U;
const Vector3 dh_row =
row % 6U < 3U
? Vector3{}
: z * rotations[mass_node]
.director_second[row % 6U - 3U][direction];
for (std::size_t local = 0U; local < 6U; ++local) {
const auto field = 6U * mass_node + local;
const Vector3 dh_field =
local < 3U
? Vector3{}
: z * rotations[mass_node]
.director_second[local - 3U][direction];
mass_derivative[mass_node][direction][row % 6U][local] +=
scale * (dh_row.Dot(h[field]) + h[row].Dot(dh_field));
scale *
mass_variation[direction][6U * (row % 6U) + local];
}
}
}
@@ -1004,8 +1016,15 @@ Result<NonlinearMitc4InertiaEvaluation> NonlinearMitc4Inertia::Evaluate(
"invalid-nonlinear-inertia-evaluation", source_location_, identity_,
"Nonlinear MITC4 inertia evaluation must remain finite.");
}
const Status physical_mass = CheckPhysicalMass(result.mass, nulls, length,
source_location_, identity_);
Status physical_mass = Status::Ok();
try {
physical_mass = CheckPhysicalMass(result.mass, nulls, length,
source_location_, identity_);
} catch (const std::exception& error) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-positive-mass", source_location_, identity_,
error.what());
}
if (!physical_mass.IsOk()) {
return Result<NonlinearMitc4InertiaEvaluation>::Failure(physical_mass);
}
+185 -26
View File
@@ -10,8 +10,34 @@
#include <utility>
#include "fesa/elements/finite_rotation.h"
#include "elements/nonlinear_mitc4_configuration_internal.h"
#include "math/dense_blas_internal.h"
namespace fesa {
nonlinear_mitc4_internal::ConfigurationSnapshot
nonlinear_mitc4_internal::ConfigurationSnapshot::Read(
const NonlinearMitc4Shell& element) {
ConfigurationSnapshot result{};
std::size_t index = 0U;
for (const auto& coordinates : element.coordinates_)
for (std::size_t c = 0U; c < 3U; ++c)
result.values[index++] = coordinates[c];
for (const auto& frame : element.initial_frames_)
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j)
result.values[index++] = frame(i, j);
for (const auto& director : element.initial_directors_)
for (std::size_t c = 0U; c < 3U; ++c) result.values[index++] = director[c];
result.values[index++] = element.thickness_;
result.values[index++] = element.youngs_modulus_;
result.values[index++] = element.poisson_ratio_;
result.values[index] = element.drilling_stiffness_;
result.source = element.source_location_;
result.identity = element.identity_;
return result;
}
namespace {
constexpr std::size_t kNodeCount = 4U;
@@ -394,22 +420,66 @@ constexpr std::size_t kHessianSize = kGlobalDofCount * kGlobalDofCount;
/// @brief Carries exact first and second trial-coordinate variations.
class Hessian {
public:
Hessian() : values_{std::make_shared<std::array<double, kHessianSize>>()} {}
Hessian() = default;
double& operator[](const std::size_t index) { return (*values_)[index]; }
double& operator[](const std::size_t index) { return Data()[index]; }
const double& operator[](const std::size_t index) const {
return (*values_)[index];
return Storage()[index];
}
auto begin() const { return values_->begin(); }
auto begin() const { return Storage().begin(); }
auto end() const { return values_->end(); }
auto end() const { return Storage().end(); }
double* Data() {
if (!values_) {
values_ = std::make_shared<Values>();
values_->entries.fill(0.0);
}
return values_->entries.data();
}
const double* ReadData() const { return Storage().data(); }
/// @brief Identifies only the immutable zero Hessian, independently of seeds.
bool IsImplicitZero() const { return !values_; }
/// @brief Gets fresh output storage without redundant zero initialization.
/// @note Only fresh operator results use this pointer; all 576 entries must
/// be written before any read or copy of the result.
double* OverwriteData() {
if (!values_) values_ = std::make_shared<Values>();
return values_->entries.data();
}
private:
std::shared_ptr<std::array<double, kHessianSize>> values_;
struct Values {
// User-provided construction leaves entries uninitialized for a complete
// output overwrite while make_shared retains the single allocation.
Values() noexcept {}
std::array<double, kHessianSize> entries;
};
const std::array<double, kHessianSize>& Storage() const {
// Constants borrow only this immutable zero. Mutable coefficients retain
// the existing shared ownership; no trial-dependent cache is introduced.
static const std::array<double, kHessianSize> kZero{};
return values_ ? values_->entries : kZero;
}
std::shared_ptr<Values> values_;
};
/// @brief Forms an ordinary derivative outer product without a Matrix copy.
/// @note Rejected nonfinite operands propagate to the existing Jet finite gate.
void Outer(const std::array<double, kGlobalDofCount>& left,
const std::array<double, kGlobalDofCount>& right, double* result) {
if (!dense_blas_internal::OuterProduct(left.data(), left.size(), right.data(),
right.size(), result))
std::fill_n(result, kHessianSize, std::numeric_limits<double>::quiet_NaN());
}
/// @brief Carries exact first and second trial-coordinate variations.
struct Jet {
double value{};
@@ -419,6 +489,9 @@ struct Jet {
double directional_second{};
std::array<double, kGlobalDofCount> mixed_first{};
std::array<double, kGlobalDofCount> mixed_second{};
// Provenance for numerically zero derivatives; a null Hessian alone is not
// constant provenance because Variable retains a nonzero gradient seed.
bool constant_derivatives{false};
};
using JetVector3 = std::array<Jet, 3>;
@@ -429,6 +502,7 @@ using JetTensor = std::array<std::array<Jet, 3>, 3>;
Jet Constant(const double value) {
Jet result{};
result.value = value;
result.constant_derivatives = true;
return result;
}
@@ -436,6 +510,7 @@ Jet Constant(const double value) {
Jet Variable(const double value, const std::size_t index,
const double direction = 0.0) {
Jet result = Constant(value);
result.constant_derivatives = false;
result.first[index] = 1.0;
result.directional_first = direction;
return result;
@@ -455,8 +530,16 @@ Jet operator+(const Jet& left, const Jet& right) {
result.mixed_second[index] =
left.mixed_second[index] + right.mixed_second[index];
}
result.constant_derivatives = left.constant_derivatives &&
right.constant_derivatives &&
std::isfinite(result.value);
if (left.second.IsImplicitZero() && right.second.IsImplicitZero())
return result;
const double* left_second = left.second.ReadData();
const double* right_second = right.second.ReadData();
double* result_second = result.second.OverwriteData();
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = left.second[index] + right.second[index];
result_second[index] = left_second[index] + right_second[index];
}
return result;
}
@@ -475,8 +558,16 @@ Jet operator-(const Jet& left, const Jet& right) {
result.mixed_second[index] =
left.mixed_second[index] - right.mixed_second[index];
}
result.constant_derivatives = left.constant_derivatives &&
right.constant_derivatives &&
std::isfinite(result.value);
if (left.second.IsImplicitZero() && right.second.IsImplicitZero())
return result;
const double* left_second = left.second.ReadData();
const double* right_second = right.second.ReadData();
double* result_second = result.second.OverwriteData();
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = left.second[index] - right.second[index];
result_second[index] = left_second[index] - right_second[index];
}
return result;
}
@@ -506,13 +597,38 @@ Jet operator*(const Jet& left, const Jet& right) {
2.0 * left.directional_first * right.mixed_first[index] +
left.value * right.mixed_second[index];
}
const bool finite_values = std::isfinite(left.value) &&
std::isfinite(right.value) &&
std::isfinite(result.value);
const auto finite_gradient = [](const auto& gradient) {
return std::all_of(gradient.begin(), gradient.end(),
[](const double value) { return std::isfinite(value); });
};
const bool zero_outer = finite_values &&
((left.constant_derivatives && finite_gradient(right.first)) ||
(right.constant_derivatives && finite_gradient(left.first)));
result.constant_derivatives = finite_values && left.constant_derivatives &&
right.constant_derivatives;
if (zero_outer && left.second.IsImplicitZero() &&
right.second.IsImplicitZero()) return result;
const double* left_second = left.second.ReadData();
const double* right_second = right.second.ReadData();
double* result_second = result.second.OverwriteData();
if (zero_outer) {
for (std::size_t index = 0U; index < kHessianSize; ++index)
result_second[index] = left_second[index] * right.value + 0.0 + 0.0 +
left.value * right_second[index];
return result;
}
Outer(left.first, right.first, result_second);
std::array<double, kHessianSize> reverse;
Outer(right.first, left.first, reverse.data());
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result.second[index] = left.second[index] * right.value +
left.first[row] * right.first[column] +
left.first[column] * right.first[row] +
left.value * right.second[index];
result_second[index] = left_second[index] * right.value +
result_second[index] + reverse[index] +
left.value * right_second[index];
}
}
return result;
@@ -529,8 +645,14 @@ Jet operator*(const double scalar, const Jet& value) {
result.mixed_first[index] = scalar * value.mixed_first[index];
result.mixed_second[index] = scalar * value.mixed_second[index];
}
result.constant_derivatives = value.constant_derivatives &&
std::isfinite(scalar) &&
std::isfinite(result.value);
if (std::isfinite(scalar) && value.second.IsImplicitZero()) return result;
const double* value_second = value.second.ReadData();
double* result_second = result.second.OverwriteData();
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = scalar * value.second[index];
result_second[index] = scalar * value_second[index];
}
return result;
}
@@ -559,11 +681,20 @@ Jet Compose(const Jet& input, const double value, const double first,
input.first[index] * input.directional_second) +
first * input.mixed_second[index];
}
result.constant_derivatives = input.constant_derivatives &&
std::isfinite(input.value) && std::isfinite(value) &&
std::isfinite(first) && std::isfinite(second) && std::isfinite(third);
if (result.constant_derivatives) return result;
std::array<double, kGlobalDofCount> scaled_first{};
for (std::size_t row = 0U; row < kGlobalDofCount; ++row)
scaled_first[row] = second * input.first[row];
const double* input_second = input.second.ReadData();
double* result_second = result.second.OverwriteData();
Outer(scaled_first, input.first, result_second);
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result.second[index] = first * input.second[index] +
second * input.first[row] * input.first[column];
result_second[index] = first * input_second[index] + result_second[index];
}
}
return result;
@@ -613,19 +744,38 @@ Jet Atan2(const Jet& y, const Jet& x) {
const double derivative_yx =
(y.value * y.value - x.value * x.value) / radius_fourth;
const double derivative_xx = 2.0 * x.value * y.value / radius_fourth;
const bool zero_hessian = x.constant_derivatives && y.constant_derivatives &&
std::isfinite(result.value) && std::isfinite(derivative_y) &&
std::isfinite(derivative_x) && std::isfinite(derivative_yy) &&
std::isfinite(derivative_yx) && std::isfinite(derivative_xx);
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] =
derivative_y * y.first[index] + derivative_x * x.first[index];
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result.second[index] = derivative_y * y.second[index] +
derivative_x * x.second[index] +
derivative_yy * y.first[row] * y.first[column] +
derivative_yx * (y.first[row] * x.first[column] +
x.first[row] * y.first[column]) +
derivative_xx * x.first[row] * x.first[column];
if (!zero_hessian) {
std::array<double, kGlobalDofCount> scaled_y{};
std::array<double, kGlobalDofCount> scaled_x{};
for (std::size_t i = 0U; i < kGlobalDofCount; ++i) {
scaled_y[i] = derivative_yy * y.first[i];
scaled_x[i] = derivative_xx * x.first[i];
}
std::array<std::array<double, kHessianSize>, 4U> products;
Outer(scaled_y, y.first, products[0U].data());
Outer(y.first, x.first, products[1U].data());
Outer(x.first, y.first, products[2U].data());
Outer(scaled_x, x.first, products[3U].data());
const double* y_second = y.second.ReadData();
const double* x_second = x.second.ReadData();
double* result_second = result.second.OverwriteData();
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result_second[index] =
derivative_y * y_second[index] + derivative_x * x_second[index] +
products[0U][index] +
derivative_yx * (products[1U][index] + products[2U][index]) +
products[3U][index];
}
}
}
// Use a bounded ratio for the directional third derivative only. Retain
@@ -644,6 +794,7 @@ Jet Atan2(const Jet& y, const Jet& x) {
result.mixed_first[i] = sign * angle.mixed_first[i];
result.mixed_second[i] = sign * angle.mixed_second[i];
}
result.constant_derivatives = zero_hessian && angle.constant_derivatives;
return result;
}
@@ -1181,10 +1332,14 @@ Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
DrillSpinRate(trial.rotation[node], initial_directors[node]) * gamma;
}
energy = energy + 0.5 * drilling_stiffness * gamma * gamma;
std::array<double, kGlobalDofCount> scaled_gradient{};
for (std::size_t row = 0U; row < kGlobalDofCount; ++row)
scaled_gradient[row] = drilling_stiffness * gamma.first[row];
std::array<double, kHessianSize> contribution;
Outer(scaled_gradient, gamma.first, contribution.data());
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
(*metric)(row, column) +=
drilling_stiffness * gamma.first[row] * gamma.first[column];
(*metric)(row, column) += contribution[row * kGlobalDofCount + column];
}
}
}
@@ -1505,6 +1660,7 @@ NonlinearMitc4Shell::EvaluateExternalDrillingConstraint(
const std::size_t node, const Vector3& phi, const Vector3& phi_dot,
const Vector3& force, const Vector3& force_rate,
const Vector3& force_acceleration) const {
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
if (node >= kNodeCount || !phi_dot.IsFinite() || !force.IsFinite() ||
!force_rate.IsFinite() || !force_acceleration.IsFinite()) {
return Failure<NonlinearMitc4ExternalDrillingConstraint>(
@@ -1594,6 +1750,9 @@ NonlinearMitc4Shell::EvaluateExternalDrillingConstraint(
Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial,
const Vector* coordinate_direction) {
// Element-local MKL work uses one worker only when the caller has no TLS
// override. The guard restores that request before returning to global solve.
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
if (coordinate_direction != nullptr) {
if (coordinate_direction->Size() != kGlobalDofCount) {
return Failure<NonlinearMitc4InternalEquilibrium>(
+73 -5
View File
@@ -1,5 +1,6 @@
#include "math/dense_blas_internal.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
@@ -26,8 +27,27 @@ ScopedSingleEquationThreads::~ScopedSingleEquationThreads() {
if (restore_default_) static_cast<void>(mkl_set_num_threads_local(0));
}
ScopedLocalDenseThreads::ScopedLocalDenseThreads() noexcept {
const int previous = mkl_set_num_threads_local(1);
if (previous == 0) {
restore_default_ = true;
} else {
static_cast<void>(mkl_set_num_threads_local(previous));
}
}
ScopedLocalDenseThreads::~ScopedLocalDenseThreads() {
if (restore_default_) static_cast<void>(mkl_set_num_threads_local(0));
}
LocalDensePolicy ReadScopedLocalDensePolicy() noexcept {
const ScopedLocalDenseThreads scope{};
return {mkl_domain_get_max_threads(MKL_DOMAIN_BLAS), mkl_get_dynamic()};
}
bool CholeskyLowerAndInverse(const Matrix& matrix, Matrix& lower,
Matrix& inverse_lower) {
Matrix& inverse_lower, CholeskyFailure* failure) {
if (failure != nullptr) *failure = CholeskyFailure::kNone;
if (matrix.Rows() != matrix.Columns())
throw std::invalid_argument{"Cholesky requires a square matrix."};
const auto size = ToMklSize(matrix.Rows());
@@ -48,17 +68,30 @@ bool CholeskyLowerAndInverse(const Matrix& matrix, Matrix& lower,
values[row * matrix.Columns() + column] = matrix(row, column);
}
if (LAPACKE_dpotrf(LAPACK_ROW_MAJOR, 'L', size.Value(), values.data(),
size.Value()) != 0) return false;
size.Value()) != 0) {
if (failure != nullptr) *failure = CholeskyFailure::kPivot;
return false;
}
for (std::size_t row = 0; row < matrix.Rows(); ++row)
for (std::size_t column = 0; column <= row; ++column)
for (std::size_t column = 0; column <= row; ++column) {
candidate_lower(row, column) = values[row * matrix.Columns() + column];
if (!std::isfinite(candidate_lower(row, column))) {
if (failure != nullptr) *failure = CholeskyFailure::kPivot;
return false;
}
}
if (LAPACKE_dtrtri(LAPACK_ROW_MAJOR, 'L', 'N', size.Value(), values.data(),
size.Value()) != 0) return false;
size.Value()) != 0) {
if (failure != nullptr) *failure = CholeskyFailure::kInverse;
return false;
}
for (std::size_t row = 0; row < matrix.Rows(); ++row)
for (std::size_t column = 0; column <= row; ++column) {
const auto value = values[row * matrix.Columns() + column];
if (!std::isfinite(candidate_lower(row, column)) || !std::isfinite(value))
if (!std::isfinite(value)) {
if (failure != nullptr) *failure = CholeskyFailure::kInverse;
return false;
}
candidate_inverse(row, column) = value;
}
lower = std::move(candidate_lower);
@@ -96,6 +129,41 @@ Matrix MultiplyTransposeLeft(const Matrix& left, const Matrix& right) {
return transposed.Multiply(right);
}
bool OuterProduct(const double* left, const std::size_t rows,
const double* right, const std::size_t columns,
double* destination) {
return MultiplyTransposeRight(left, rows, right, columns, 1U, destination);
}
bool MultiplyTransposeRight(const double* left, const std::size_t rows,
const double* right, const std::size_t columns,
const std::size_t inner, double* destination) {
const auto m = ToMklSize(rows);
const auto n = ToMklSize(columns);
const auto k = ToMklSize(inner);
const auto maximum = std::numeric_limits<std::size_t>::max() / sizeof(double);
if (!m.HasValue() || !n.HasValue() || !k.HasValue() ||
(columns != 0U && rows > maximum / columns) ||
(inner != 0U && (rows > maximum / inner || columns > maximum / inner)))
throw std::length_error{"Dense product dimensions exceed backend limits."};
if (rows == 0U || columns == 0U) return true;
if (destination == nullptr ||
(inner != 0U && (left == nullptr || right == nullptr)))
throw std::invalid_argument{"Dense product requires nonnull storage."};
if (inner == 0U) {
std::fill_n(destination, rows * columns, 0.0);
return true;
}
for (std::size_t i = 0U; i < rows * inner; ++i)
if (!std::isfinite(left[i])) return false;
for (std::size_t i = 0U; i < columns * inner; ++i)
if (!std::isfinite(right[i])) return false;
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasTrans, m.Value(), n.Value(),
k.Value(), 1.0, left, k.Value(), right, k.Value(), 0.0,
destination, n.Value());
return true;
}
Result<MKL_INT> ToMklSize(const std::size_t size) {
if (size > static_cast<std::size_t>((std::numeric_limits<MKL_INT>::max)())) {
return Result<MKL_INT>::Failure(Status::Failure(
+52 -3
View File
@@ -1,10 +1,10 @@
#ifndef FESA_SRC_FESA_MATH_DENSE_BLAS_INTERNAL_H_
#define FESA_SRC_FESA_MATH_DENSE_BLAS_INTERNAL_H_
#include <mkl.h>
#include <cstddef>
#include <mkl.h>
#include "fesa/core/status.h"
#include "fesa/math/matrix.h"
#include "fesa/math/vector.h"
@@ -26,6 +26,30 @@ class ScopedSingleEquationThreads {
bool restore_default_{false};
};
/// @brief Limits element-local dense work under the caller's default policy.
/// @note Positive caller TLS overrides are retained. Default requests are
/// restored on success and unwind; process-wide settings are never changed.
class ScopedLocalDenseThreads {
public:
ScopedLocalDenseThreads() noexcept;
ScopedLocalDenseThreads(const ScopedLocalDenseThreads&) = delete;
ScopedLocalDenseThreads& operator=(const ScopedLocalDenseThreads&) = delete;
~ScopedLocalDenseThreads();
private:
bool restore_default_{false};
};
/// @brief Records requested backend policy, without claiming actual threads.
struct LocalDensePolicy {
int blas_max_threads;
int dynamic;
};
/// @brief Reads BLAS policy inside the same restored scope as local elements.
/// @note No process-wide policy is changed; positive caller TLS is retained.
LocalDensePolicy ReadScopedLocalDensePolicy() noexcept;
/// @brief Converts a dense storage length to the private MKL integer type.
/// @return The converted length or an uncategorized overflow failure.
Result<MKL_INT> ToMklSize(std::size_t size);
@@ -39,13 +63,38 @@ Matrix MultiplyTransposeLeft(const Matrix& left, const Matrix& right);
/// @throws std::length_error if the size exceeds the backend integer range.
Vector MultiplyDiagonal(const Vector& diagonal, const Vector& rhs);
/// @brief Writes one row-major outer product through MKL without allocation.
/// @note Source arrays and destination must not alias. A nonfinite input
/// returns false before writing; finite product overflow remains for the
/// owner's gate. A zero dimension accepts null pointers and performs no backend
/// call.
/// @throws std::invalid_argument for null storage of a nonempty product.
/// @throws std::length_error for dimensions outside the backend integer range.
bool OuterProduct(const double* left, std::size_t rows, const double* right,
std::size_t columns, double* destination);
/// @brief Writes left times right-transpose to nonaliased contiguous storage.
/// @note Inputs are row-major rows-by-inner and columns-by-inner. Nonfinite
/// input returns false before writing. Finite output overflow is not hidden.
/// Zero output dimensions accept null pointers; inner zero writes exact zeros.
/// @throws std::invalid_argument for missing nonempty storage.
/// @throws std::length_error for dimension or storage-size overflow.
bool MultiplyTransposeRight(const double* left, std::size_t rows,
const double* right, std::size_t columns,
std::size_t inner, double* destination);
/// @brief Distinguishes positive-factor failure from inverse failure.
enum class CholeskyFailure { kNone, kPivot, kInverse };
/// @brief Computes a lower Cholesky factor and its inverse with MKL LAPACK.
/// @return False for a nonpositive pivot or nonfinite factor/inverse.
/// @note Outputs are replaced only after both finite candidates succeed.
/// The optional failure output is initialized even when validation throws.
/// @throws std::invalid_argument for a nonsquare or nonfinite input.
/// @throws std::length_error for a backend size overflow.
bool CholeskyLowerAndInverse(const Matrix& matrix, Matrix& lower,
Matrix& inverse_lower);
Matrix& inverse_lower,
CholeskyFailure* failure = nullptr);
/// @brief Copies contiguous double values through the private BLAS backend.
/// @param source Readable storage for size values when size is nonzero.