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.
This commit is contained in:
KOKO\Mimi
2026-10-13 16:57:19 +09:00
parent ede137c6b4
commit eaa5cbeb21
110 changed files with 15274 additions and 971 deletions
@@ -1,8 +1,10 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
@@ -11,6 +13,7 @@
#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"
@@ -24,7 +27,8 @@ struct Fixture {
Fixture MakeFixture(const std::size_t free_nodes = 1U,
const bool partial_rotation = false,
const bool translations_free = true) {
const bool translations_free = true,
const double youngs_modulus = 1000.0) {
fesa::ModelDefinition definition{};
definition.source_path = "hht-algebra.inp";
definition.nodes = {
@@ -32,7 +36,8 @@ Fixture MakeFixture(const std::size_t free_nodes = 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", 1000, 0.3, {definition.source_path, 5U}}};
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}}};
@@ -106,8 +111,11 @@ fesa::hht_internal::DynamicAssembly Assembly(
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;
@@ -119,12 +127,16 @@ fesa::hht_internal::DynamicAssembly Assembly(
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),
@@ -134,7 +146,318 @@ fesa::hht_internal::DynamicAssembly Assembly(
0.0,
0.0,
fesa::Vector{4U},
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,
@@ -169,6 +492,20 @@ TEST(HhtDynamicAlgebra,
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;
@@ -240,6 +577,11 @@ TEST(HhtDynamicAlgebra, AppliesTheExactRegularityConditionBoundary) {
}
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)