Files
FESADev/tests/unit/analysis/hht_dynamic_algebra_test.cpp
T
KOKO\Mimi eaa5cbeb21 feat: adopt lumped mass and reduced-coordinate shell HHT
Integrate Hex8 mass verification and fixed-null-basis shell dynamics with numerical and physics handoff evidence. Preserve interrupted reference and full-validation status; full integration validation is deferred until after performance optimization.
2026-10-13 16:57:19 +09:00

673 lines
29 KiB
C++

#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/math/matrix.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
namespace {
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
fesa::DofManager dofs;
};
Fixture MakeFixture(const std::size_t free_nodes = 1U,
const bool partial_rotation = false,
const bool translations_free = true,
const double youngs_modulus = 1000.0) {
fesa::ModelDefinition definition{};
definition.source_path = "hht-algebra.inp";
definition.nodes = {
{{"Plate", 1, "1"}, {-1, -1, 0}, {definition.source_path, 1U}},
{{"Plate", 2, "2"}, {1, -1, 0}, {definition.source_path, 2U}},
{{"Plate", 3, "3"}, {1, 1, 0}, {definition.source_path, 3U}},
{{"Plate", 4, "4"}, {-1, 1, 0}, {definition.source_path, 4U}}};
definition.materials = {
{"Elastic", youngs_modulus, 0.3, {definition.source_path, 5U}}};
definition.densities = {{0U, 1.0, {definition.source_path, 6U}}};
definition.shell_sections = {
{"Plate", 0.2, 0U, {definition.source_path, 7U}}};
definition.shell_elements = {{{"Plate", 1, "1"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{definition.source_path, 8U}}};
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "Dynamic";
step.source_step_name_canonical = "DYNAMIC";
step.initial_increment = 0.1;
step.time_period = 0.1;
step.increment_count = 1U;
step.maximum_increments = 1U;
for (std::size_t i = 0; i < 4; ++i) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(i), {0, 0, 1}, {1, 0, 0}, {0, 1, 0}});
const std::string name = "Node" + std::to_string(i);
definition.node_sets.push_back({name,
std::nullopt,
{static_cast<fesa::EntityIndex>(i)},
{definition.source_path, 9U}});
if (i >= free_nodes) {
step.boundaries.push_back(
{name, 1, 6, 0.0, {definition.source_path, 10U}});
} else {
if (partial_rotation)
step.boundaries.push_back(
{name, 4, 5, 0.0, {definition.source_path, 10U}});
if (!translations_free)
step.boundaries.push_back(
{name, 1, 3, 0.0, {definition.source_path, 10U}});
}
}
definition.dynamic_step = step;
auto result = fesa::Domain::Create(std::move(definition));
if (!result.HasValue()) throw std::runtime_error{"Invalid algebra domain."};
auto domain = std::make_unique<fesa::Domain>(std::move(result.Value()));
auto model = fesa::AnalysisModel::Create(*domain);
if (!model.HasValue()) throw std::runtime_error{"Invalid algebra model."};
auto dofs = fesa::DofManager::Create(model.Value());
if (!dofs.HasValue()) throw std::runtime_error{"Invalid algebra DOFs."};
return {std::move(domain), std::move(dofs.Value())};
}
fesa::SparseMatrix Sparse(const fesa::Matrix& matrix) {
std::vector<fesa::CooContribution> entries;
for (std::size_t i = 0; i < matrix.Rows(); ++i) {
for (std::size_t j = 0; j < matrix.Columns(); ++j) {
entries.push_back({i, j, matrix(i, j), 0U, entries.size()});
}
}
auto result = fesa::hht_internal::MakeSparse(matrix.Rows(), matrix.Columns(),
std::move(entries));
EXPECT_TRUE(result.HasValue());
return std::move(result.Value());
}
fesa::hht_internal::DynamicAssembly Assembly(
const double second_stiffness = 3.0, const double mass_scale = 1.0) {
fesa::DynamicAcceptedFrameState frame;
frame.is_initial = true;
for (auto* vector :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.inertia_force,
&frame.internal_force, &frame.external_force, &frame.residual,
&frame.residual_scale})
*vector = fesa::Vector{24U};
frame.external_force[0] = 2.0;
frame.residual_scale[0] = 2.0;
fesa::Matrix mass{24U, 24U};
fesa::Matrix position{24U, 24U};
fesa::Matrix velocity{24U, 24U};
fesa::Matrix stiffness{24U, 24U};
fesa::Matrix metric{24U, 24U};
fesa::Matrix hidden_gradient{4U, 24U};
std::vector<fesa::DynamicRotationKinematics> rotations;
for (std::size_t i = 0; i < 4; ++i) {
const std::size_t base = 6U * i;
for (std::size_t j = 0; j < 5; ++j)
mass(base + j, base + j) = mass_scale * (j < 3U ? 2.0 : 1.0);
const double k = i == 1U ? second_stiffness : 3.0;
stiffness(base, base) = 8.0 + k * 0.7 * 0.7;
stiffness(base, base + 5U) = -0.7 * k;
stiffness(base + 5U, base) = -0.7 * k;
stiffness(base + 5U, base + 5U) = k;
metric(base + 5U, base + 5U) = 3.0;
hidden_gradient(i, base) = -0.7 * k;
hidden_gradient(i, base + 5U) = k;
auto rotation = fesa::EvaluateDynamicRotation({}, {0, 0, 1}, {}, {});
EXPECT_TRUE(rotation.HasValue());
rotations.push_back(std::move(rotation.Value()));
}
return {std::move(frame),
Sparse(mass),
Sparse(position),
Sparse(velocity),
Sparse(stiffness),
Sparse(stiffness),
Sparse(metric),
fesa::Vector{24U},
fesa::Vector{24U},
std::move(rotations),
0.0,
0.0,
fesa::Vector{4U},
fesa::Vector{4U},
Sparse(hidden_gradient)};
}
fesa::DynamicAcceptedFrameState EmptyTrial(const Fixture& fixture) {
fesa::DynamicAcceptedFrameState frame{};
for (auto* values :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.external_force})
*values = fesa::Vector(fixture.dofs.FullDofCount());
return frame;
}
TEST(HhtNewtonStart, ValidPredictorPreservesRatesFixedDofsAndOldState) {
const auto fixture = MakeFixture(4U, true);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
auto old = EmptyTrial(fixture);
old.velocity[2U] = 0.2;
old.velocity[5U] = 0.1;
old.acceleration[2U] = 0.3;
old.algorithmic_acceleration[2U] = 0.3;
old.iteration_count = 7U;
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
const auto predictor = fesa::PredictHht(
old.generalized_coordinates, old.velocity, old.algorithmic_acceleration,
0.1, parameters).Value();
fesa::SerialParallelFor parallel;
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
ASSERT_TRUE(start.HasValue());
const auto& frame = start.Value().frame;
EXPECT_GT(frame.generalized_coordinates[2U], 0.0);
EXPECT_EQ(frame.iteration_count, 0U);
for (std::size_t i = 0U; i < 24U; ++i) {
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[i], predictor.coordinates[i]);
EXPECT_DOUBLE_EQ(frame.velocity[i], predictor.velocity[i]);
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[i], 0.0);
EXPECT_DOUBLE_EQ(frame.acceleration[i],
parameters.alpha * old.acceleration[i] /
(1.0 + parameters.alpha));
EXPECT_DOUBLE_EQ(old.generalized_coordinates[i], 0.0);
}
for (const auto i : fixture.dofs.ConstrainedDofs()) {
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[i], 0.0);
EXPECT_DOUBLE_EQ(frame.velocity[i], 0.0);
EXPECT_DOUBLE_EQ(frame.acceleration[i], 0.0);
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[i], 0.0);
}
EXPECT_DOUBLE_EQ(old.velocity[2U], 0.2);
EXPECT_DOUBLE_EQ(old.acceleration[2U], 0.3);
EXPECT_EQ(old.iteration_count, 7U);
}
TEST(HhtNewtonStart, InvalidPredictorGeometryUsesUnchangedAcceptedCoordinates) {
const auto fixture = MakeFixture(4U);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const auto old = EmptyTrial(fixture);
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
fesa::SerialParallelFor parallel;
for (const int invalid_case : {0, 1, 2}) {
SCOPED_TRACE(invalid_case);
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
if (invalid_case == 0) {
predictor.coordinates[5U] = 3.5;
} else if (invalid_case == 1) {
predictor.coordinates[6U] = -2.0;
predictor.coordinates[12U] = -2.0;
} else {
predictor.coordinates[3U] =
std::acos(-1.0) - 2.0 * std::sqrt(std::numeric_limits<double>::epsilon());
}
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
ASSERT_TRUE(start.HasValue());
for (std::size_t i = 0U; i < 24U; ++i) {
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[i], 0.0);
EXPECT_DOUBLE_EQ(old.generalized_coordinates[i], 0.0);
const double auxiliary =
-predictor.coordinates[i] / (parameters.beta * 0.1 * 0.1);
EXPECT_DOUBLE_EQ(start.Value().frame.algorithmic_acceleration[i], auxiliary);
EXPECT_DOUBLE_EQ(start.Value().frame.acceleration[i],
auxiliary / (1.0 + parameters.alpha));
}
}
}
TEST(HhtNewtonStart, ValidChartRateOverflowIsNotReplacedByAcceptedGeometry) {
const auto fixture = MakeFixture(4U);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const auto old = EmptyTrial(fixture);
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
predictor.coordinates[3U] = 0.1;
predictor.velocity[3U] = 1e200;
fesa::SerialParallelFor parallel;
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), 1e-160,
fesa::MakeHhtParameters(-0.05).Value(), parallel);
ASSERT_FALSE(start.HasValue());
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
"invalid-dynamic-rotation");
}
TEST(HhtNewtonStart, LaterOutsideChartNodeDoesNotHideEarlierRateOverflow) {
const auto fixture = MakeFixture(4U);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const auto old = EmptyTrial(fixture);
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
predictor.coordinates[3U] = 0.1;
predictor.velocity[3U] = 1e200;
predictor.coordinates[11U] = 3.5;
fesa::SerialParallelFor parallel;
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), 1e-160,
fesa::MakeHhtParameters(-0.05).Value(), parallel);
ASSERT_FALSE(start.HasValue());
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
"invalid-dynamic-rotation");
}
TEST(HhtNewtonStart, UsesTheExistingPrincipalChartMarginForInitialSelection) {
const auto fixture = MakeFixture(4U);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const auto old = EmptyTrial(fixture);
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
const double margin = std::sqrt(std::numeric_limits<double>::epsilon());
fesa::SerialParallelFor parallel;
for (const bool inside : {true, false}) {
SCOPED_TRACE(inside);
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
const double angle = std::acos(-1.0) - (inside ? 2.0 : 0.5) * margin;
for (std::size_t node = 0U; node < 4U; ++node) {
const auto& x = fixture.domain->Nodes()[node].coordinates;
predictor.coordinates[6U * node] =
(std::cos(angle) - 1.0) * x[0U] - std::sin(angle) * x[1U];
predictor.coordinates[6U * node + 1U] =
std::sin(angle) * x[0U] + (std::cos(angle) - 1.0) * x[1U];
predictor.coordinates[6U * node + 5U] = angle;
}
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
ASSERT_TRUE(start.HasValue());
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U],
inside ? predictor.coordinates[5U] : 0.0);
}
}
TEST(HhtNewtonStart, CommonEnergyScalingSelectsTheSmallerFreeResidual) {
const auto fixture = MakeFixture(4U);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const auto old = EmptyTrial(fixture);
auto probe = old;
probe.generalized_coordinates[5U] = 0.25;
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
fesa::SerialParallelFor parallel;
const auto evaluated = fesa::hht_internal::EvaluateAssembly(
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
0.1, parameters, parallel, false);
ASSERT_TRUE(evaluated.HasValue());
double drill_residual = 0.0;
for (std::size_t i = 3U; i < 24U; i += 6U)
for (std::size_t j = 0U; j < 3U; ++j)
drill_residual = std::max(
drill_residual, std::abs(evaluated.Value().frame.residual[i + j]));
ASSERT_GT(drill_residual, 0.0);
// A rigid translation gives m*a per node, m=rho*t*area/4=0.2.
// The accepted-start force is 0.75 of the predictor torque. Multiplying
// force by ell=2 reverses the unscaled force-versus-torque comparison.
const double translation =
0.75 * drill_residual * (1.0 + parameters.alpha) * parameters.beta *
0.1 * 0.1 / 0.2;
fesa::HhtPredictor predictor{probe.generalized_coordinates, fesa::Vector(24U)};
for (std::size_t node = 0U; node < 4U; ++node)
predictor.coordinates[6U * node] = translation;
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
ASSERT_TRUE(start.HasValue());
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U], 0.25);
}
TEST(HhtNewtonStart, ConstrainedReactionDoesNotChangeNontrivialFreeResidualTie) {
const auto fixture = MakeFixture();
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
auto old = EmptyTrial(fixture);
auto probe = old;
probe.generalized_coordinates[5U] = 0.25;
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
fesa::SerialParallelFor parallel;
const auto evaluated = fesa::hht_internal::EvaluateAssembly(
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
0.1, parameters, parallel, false);
ASSERT_TRUE(evaluated.HasValue());
fesa::Vector load_scale(24U);
for (std::size_t i = 0U; i < 24U; ++i) {
old.external_force[i] = 0.5 * evaluated.Value().frame.internal_force[i];
load_scale[i] = std::abs(old.external_force[i]);
}
// Pure drill spin has zero physical inertia at both starting points, so
// the free residuals are exact opposites. A support reaction is excluded.
old.external_force[6U] = load_scale[6U] = 1e308;
fesa::HhtPredictor predictor{probe.generalized_coordinates, fesa::Vector(24U)};
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, load_scale, 0.1, parameters, parallel);
ASSERT_TRUE(start.HasValue());
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U], 0.25);
}
TEST(HhtNewtonStart, ScaledResidualOverflowRemainsAFailure) {
const auto fixture = MakeFixture(4U);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
auto old = EmptyTrial(fixture);
old.external_force[0U] = 1e308;
fesa::Vector load_scale(24U);
load_scale[0U] = 1e308;
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
predictor.coordinates[5U] = 0.25;
fesa::SerialParallelFor parallel;
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, load_scale, 0.1,
fesa::MakeHhtParameters(-0.05).Value(), parallel);
ASSERT_FALSE(start.HasValue());
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
"dynamic-invalid-state");
}
TEST(HhtNewtonStart, AcceptedStartArithmeticFailureIsNotIgnoredDuringComparison) {
const auto fixture = MakeFixture(4U);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const auto old = EmptyTrial(fixture);
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
for (std::size_t node = 0U; node < 4U; ++node)
predictor.coordinates[6U * node] = 1e10;
const double time_increment = 1e-150;
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
fesa::SerialParallelFor parallel;
auto probe = old;
probe.generalized_coordinates = predictor.coordinates;
const auto valid_predictor = fesa::hht_internal::EvaluateAssembly(
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
time_increment, parameters, parallel, false);
ASSERT_TRUE(valid_predictor.HasValue());
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), time_increment, parameters, parallel);
ASSERT_FALSE(start.HasValue());
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
"dynamic-invalid-state");
}
TEST(HhtNewtonStart, RejectedPredictorDoesNotHideInvalidAcceptedStartRates) {
const auto fixture = MakeFixture(4U);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const auto old = EmptyTrial(fixture);
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
predictor.coordinates[5U] = 3.5;
fesa::SerialParallelFor parallel;
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), 1e-160,
fesa::MakeHhtParameters(-0.05).Value(), parallel);
ASSERT_FALSE(start.HasValue());
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
"dynamic-invalid-state");
EXPECT_DOUBLE_EQ(old.generalized_coordinates[5U], 0.0);
}
TEST(HhtNewtonStart, ValidGeometryStressOverflowRemainsARecoveryFailure) {
const auto fixture = MakeFixture(4U, false, true, 1e307);
const auto shells = fesa::hht_internal::BuildRuntimeShells(
*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const auto old = EmptyTrial(fixture);
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
for (std::size_t node = 0U; node < 4U; ++node)
for (std::size_t component = 0U; component < 2U; ++component)
predictor.coordinates[6U * node + component] =
9.0 * fixture.domain->Nodes()[node].coordinates[component];
fesa::SerialParallelFor parallel;
const auto start = fesa::hht_internal::EvaluateNewtonStart(
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
old.acceleration, fesa::Vector(24U), 0.1,
fesa::MakeHhtParameters(-0.05).Value(), parallel);
ASSERT_FALSE(start.HasValue());
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
"invalid-nonlinear-shell-recovery");
}
TEST(HhtDynamicAlgebra,
InitializesCoupledMasslessAccelerationFromHiddenEquation) {
const auto fixture = MakeFixture();
auto assembly = Assembly();
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto acceleration = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
ASSERT_TRUE(acceleration.HasValue());
EXPECT_NEAR(acceleration.Value()[0], 1.0, 1.0e-12);
EXPECT_NEAR(acceleration.Value()[5], 0.7, 1.0e-12);
for (const auto full : fixture.dofs.ConstrainedDofs())
EXPECT_EQ(acceleration.Value()[full], 0.0);
const auto nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
ASSERT_EQ(nodes, std::vector<std::size_t>{0U});
assembly.frame.acceleration = acceleration.Value();
auto frame = assembly.frame;
EXPECT_TRUE(fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, nodes, positive,
tangent, &frame)
.IsOk());
ASSERT_EQ(frame.hidden_acceleration_defect.Size(), 1U);
EXPECT_NEAR(frame.hidden_acceleration_defect[0], 0.0, 1.0e-12);
EXPECT_GT(frame.hidden_acceleration_defect_scale[0], 0.0);
frame.acceleration[5] = 0.0;
const auto pseudoinverse = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, nodes, positive, tangent,
&frame);
EXPECT_FALSE(pseudoinverse.IsOk());
}
TEST(HhtDynamicAlgebra, RejectsMissingCanonicalHiddenGradient) {
const auto fixture = MakeFixture();
auto assembly = Assembly();
assembly.hidden_gradient.reset();
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto acceleration = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
ASSERT_FALSE(acceleration.HasValue());
ASSERT_FALSE(acceleration.GetStatus().Diagnostics().empty());
EXPECT_EQ(acceleration.GetStatus().Diagnostics().front().code,
"dynamic-algebraic-gradient");
}
TEST(HhtDynamicAlgebra, ChecksInitialConstraintAndVelocityWithoutRepair) {
const auto fixture = MakeFixture();
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
auto inconsistent = Assembly();
inconsistent.frame.internal_force[5] = 0.3;
inconsistent.frame.residual_scale[5] = 0.3;
EXPECT_FALSE(
fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, inconsistent, 2.0, positive, tangent)
.HasValue());
inconsistent = Assembly();
inconsistent.frame.velocity[5] = 0.1;
EXPECT_FALSE(
fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, inconsistent, 2.0, positive, tangent)
.HasValue());
}
TEST(HhtDynamicAlgebra, KeepsAcceptedHiddenRateDefectsAsDiagnostics) {
const auto fixture = MakeFixture();
auto assembly = Assembly();
assembly.frame.is_initial = false;
assembly.frame.velocity[5] = 0.1;
assembly.frame.acceleration[5] = -0.2;
auto frame = assembly.frame;
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
const auto result = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, nodes, positive, tangent,
&frame);
ASSERT_TRUE(result.IsOk());
EXPECT_NEAR(frame.hidden_velocity_defect[0], 0.3, 1.0e-12);
EXPECT_NEAR(frame.hidden_acceleration_defect[0], -0.6, 1.0e-12);
}
TEST(HhtDynamicAlgebra, RejectsNegativePhysicalMassAndAlgebraicRankLoss) {
const auto fixture = MakeFixture(2U);
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto negative = Assembly(3.0, -1.0);
EXPECT_FALSE(
fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, negative, 2.0, positive, tangent)
.HasValue());
const auto singular = Assembly(0.0);
EXPECT_FALSE(
fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, singular, 2.0, positive, tangent)
.HasValue());
}
TEST(HhtDynamicAlgebra, AppliesTheExactRegularityConditionBoundary) {
const auto fixture = MakeFixture(2U);
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
for (const int exponent : {31, 32, 33}) {
auto assembly = Assembly();
// The boundary oracle is exactly diag(1,c), with W=I. Avoid a rounded
// sqrt(3) congruence when testing a binary-exact inclusive threshold.
fesa::Matrix stiffness{24U, 24U};
fesa::Matrix metric{24U, 24U};
for (std::size_t node = 0; node < 4; ++node) {
const std::size_t drill = 6U * node + 5U;
stiffness(drill, drill) = node == 1U ? std::ldexp(1.0, -exponent) : 1.0;
metric(drill, drill) = 1.0;
}
assembly.internal_tangent = Sparse(stiffness);
assembly.drilling_metric = Sparse(metric);
fesa::Matrix hidden_gradient{4U, 24U};
for (std::size_t node = 0U; node < 4U; ++node)
hidden_gradient(node, 6U * node + 5U) =
node == 1U ? std::ldexp(1.0, -exponent) : 1.0;
assembly.hidden_gradient = Sparse(hidden_gradient);
const auto result = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
EXPECT_EQ(result.HasValue(), exponent <= 32)
<< exponent
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics()[0].message);
if (exponent == 33) {
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Diagnostics()[0].code,
"dynamic-algebraic-conditioning");
}
}
}
TEST(HhtDynamicAlgebra, AllFixedModelHasAnEmptyValidAccelerationSolve) {
const auto fixture = MakeFixture(0U);
auto assembly = Assembly();
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto result = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
ASSERT_TRUE(result.HasValue());
EXPECT_EQ(result.Value().Norm(), 0.0);
auto frame = assembly.frame;
EXPECT_TRUE(fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, {}, positive,
tangent, &frame)
.IsOk());
EXPECT_EQ(frame.hidden_constraint.Size(), 0U);
}
TEST(HhtDynamicAlgebra, PartialBoundaryRetainsEveryNonzeroPhysicalDirection) {
for (const bool translations_free : {false, true}) {
const auto fixture = MakeFixture(1U, true, translations_free);
for (const double p : {0.0, 1.0 / 2000.0, 1.0e-8}) {
auto assembly = Assembly();
assembly.frame.external_force[0] = 0.0;
fesa::Matrix mass{24U, 24U};
const double nz = std::sqrt(1.0 - p * p);
for (std::size_t node = 0; node < 4; ++node) {
for (std::size_t j = 0; j < 3; ++j)
mass(6U * node + j, 6U * node + j) = 2.0;
mass(6U * node + 3U, 6U * node + 3U) = nz * nz;
mass(6U * node + 4U, 6U * node + 4U) = 1.0;
mass(6U * node + 5U, 6U * node + 5U) = p * p;
mass(6U * node + 3U, 6U * node + 5U) = -p * nz;
mass(6U * node + 5U, 6U * node + 3U) = -p * nz;
assembly.rotations[node].drill_null = {p, 0, nz};
}
assembly.mass = Sparse(mass);
const auto nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
EXPECT_EQ(nodes.size(), p == 0.0 ? 1U : 0U);
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto result = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
const bool expected = !(translations_free && p == 1.0e-8);
EXPECT_EQ(result.HasValue(), expected) << "p=" << p;
if (!expected) {
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Diagnostics()[0].code,
"dynamic-positive-mass-conditioning");
}
}
}
}
TEST(HhtDynamicAlgebra, RejectsAdmissibleNullRankChangeBeforeAcceptingFrame) {
const auto fixture = MakeFixture(1U, true);
auto assembly = Assembly();
const auto initial_nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
ASSERT_EQ(initial_nodes.size(), 1U);
assembly.rotations[0].drill_null = {1.0e-8, 0, std::sqrt(1.0 - 1.0e-16)};
auto frame = assembly.frame;
frame.hidden_constraint = fesa::Vector{1U, 23.0};
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto result = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, initial_nodes, positive,
tangent, &frame);
ASSERT_FALSE(result.IsOk());
EXPECT_EQ(result.Diagnostics()[0].code, "dynamic-mass-null-rank-change");
EXPECT_EQ(frame.hidden_constraint[0], 23.0);
}
} // namespace