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:
@@ -497,9 +497,60 @@ TEST(NonlinearMitc4Residual, ExactDirectionalThirdGradientMatchesTangentSweep) {
|
||||
CheckThirdSweep(Trial());
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual, SmallRotationPhysicalForceRetainsDrillingNullIdentity) {
|
||||
// Exact local state of C08b h=1/32768, increment4: the rotation has just
|
||||
// crossed the original 1e-4 coefficient branch and its physical null leaked.
|
||||
auto nodes = Nodes();
|
||||
nodes[1U].coordinates = {0.0, -1.0, 0.0};
|
||||
nodes[2U].coordinates = {0.0, 0.0, 0.0};
|
||||
nodes[3U].coordinates = {-1.0, 0.0, 0.0};
|
||||
const fesa::ShellSection section{"Centered", 0.2, 0U, {}};
|
||||
const fesa::LinearElasticMaterial material{"Elastic", 1000.0, 0.3, {}};
|
||||
const auto shell = fesa::NonlinearMitc4Shell::Create(NodePointers(nodes), Frames(),
|
||||
section, material);
|
||||
ASSERT_TRUE(shell.HasValue());
|
||||
const std::array<std::array<double, 6>, 4> coordinates{{
|
||||
{6.4086914072366947e-05, -7.9345703110834861e-05, -1.4343267109683367e-05, 9.7657633876829583e-05, 7.324020770488069e-05, 4.8828020264083388e-05},
|
||||
{6.4086914054701282e-05, -3.0517578102879528e-05, -9.0026855471989683e-05, 9.7658037630745924e-05, 7.3240207702551634e-05, 4.8828064949797786e-05},
|
||||
{1.5258789062740791e-05, -3.0517578125180581e-05, 7.6293945312499653e-06, 9.7658037632949931e-05, 7.3239803947312715e-05, 4.8828124536813954e-05},
|
||||
{1.5258789080115012e-05, -7.9345703132764625e-05, 8.0871582026927324e-05, 9.7657633879033603e-05, 7.3239803949641825e-05, 4.8828079851099556e-05}
|
||||
}};
|
||||
fesa::NonlinearMitc4TrialDofs trial{};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto& q = coordinates[node];
|
||||
trial.displacement[node] = {q[0U], q[1U], q[2U]};
|
||||
trial.rotation_vector[node] = {q[3U], q[4U], q[5U]};
|
||||
}
|
||||
const fesa::Vector direction(24U);
|
||||
const auto response = fesa::EvaluateInternalResidualAndTangent(shell.Value(), trial, &direction);
|
||||
ASSERT_TRUE(response.HasValue());
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
SCOPED_TRACE(node);
|
||||
const auto rotation = fesa::EvaluateDynamicRotation(trial.rotation_vector[node],
|
||||
{0.0, 0.0, 1.0}, {}, {});
|
||||
ASSERT_TRUE(rotation.HasValue());
|
||||
double physical = 0.0, physical_scale = 0.0, projected = 0.0, total_scale = 0.0;
|
||||
for (std::size_t c = 0U; c < 3U; ++c) {
|
||||
const auto full = 6U * node + 3U + c;
|
||||
const double n = rotation.Value().drill_null[c];
|
||||
physical += n * response.Value().physical_force[full];
|
||||
physical_scale += std::abs(n * response.Value().physical_force[full]);
|
||||
projected += n * response.Value().internal_force[full];
|
||||
total_scale += std::abs(n * response.Value().internal_force[full]);
|
||||
}
|
||||
ASSERT_GT(physical_scale, 0.0);
|
||||
ASSERT_GT(total_scale, 0.0);
|
||||
EXPECT_LE(std::abs(physical) / physical_scale, 1.0e-10);
|
||||
EXPECT_LE(std::abs(projected - response.Value().drilling_constraint[node]) / total_scale,
|
||||
1.0e-10);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual, ThirdGradientIsStableAcrossSmallAngleBranches) {
|
||||
for (const double angle : {1e-4 * (1 - std::ldexp(1., -10)),
|
||||
1e-4 * (1 + std::ldexp(1., -10)), 2e-4, 1e-3}) {
|
||||
1e-4 * (1 + std::ldexp(1., -10)), 2e-4, 1e-3,
|
||||
.5 * (1 - std::ldexp(1., -12)),
|
||||
.5 * (1 + std::ldexp(1., -12))}) {
|
||||
SCOPED_TRACE(angle);
|
||||
auto trial = Trial();
|
||||
for (auto& phi : trial.rotation_vector) phi = {angle, 0., 0.};
|
||||
@@ -579,4 +630,209 @@ TEST(NonlinearMitc4Residual,
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual,
|
||||
ExposesExactDrillingConstraintThirdDirectionalGradient) {
|
||||
const auto nodes = Nodes();
|
||||
const auto shell = Shell(nodes);
|
||||
const auto trial = Trial();
|
||||
const auto direction = Direction();
|
||||
const auto velocity = ToVector(direction);
|
||||
const auto exact =
|
||||
fesa::EvaluateInternalResidualAndTangent(shell, trial, &velocity);
|
||||
ASSERT_TRUE(exact.HasValue());
|
||||
constexpr double h = 1.0e-4;
|
||||
const auto plus = fesa::EvaluateInternalResidualAndTangent(
|
||||
shell, AddScaled(trial, direction, h), &velocity);
|
||||
const auto minus = fesa::EvaluateInternalResidualAndTangent(
|
||||
shell, AddScaled(trial, direction, -h), &velocity);
|
||||
ASSERT_TRUE(plus.HasValue());
|
||||
ASSERT_TRUE(minus.HasValue());
|
||||
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
ASSERT_EQ(exact.Value().drilling_constraint_third_directional_gradient[node]
|
||||
.Size(),
|
||||
24U);
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
double plus_hessian_direction = 0.0;
|
||||
double minus_hessian_direction = 0.0;
|
||||
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||
plus_hessian_direction +=
|
||||
plus.Value().drilling_constraint_hessian[node](row, column) *
|
||||
velocity[column];
|
||||
minus_hessian_direction +=
|
||||
minus.Value().drilling_constraint_hessian[node](row, column) *
|
||||
velocity[column];
|
||||
}
|
||||
const double finite_difference =
|
||||
(plus_hessian_direction - minus_hessian_direction) / (2.0 * h);
|
||||
EXPECT_NEAR(
|
||||
exact.Value().drilling_constraint_third_directional_gradient[node]
|
||||
[row],
|
||||
finite_difference, 2.0e-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual,
|
||||
ExternalDrillingConstraintIncludesForceTimeAndCurvatureTerms) {
|
||||
const auto nodes = Nodes();
|
||||
const auto shell = Shell(nodes);
|
||||
const fesa::Vector3 phi{.6, -.2, .1};
|
||||
const fesa::Vector3 phi_dot{.2, -.1, .3};
|
||||
const fesa::Vector3 force{1.7, -.8, .4};
|
||||
const fesa::Vector3 force_rate{-.3, .6, .2};
|
||||
const fesa::Vector3 force_acceleration{.5, -.4, .1};
|
||||
const auto exact = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, phi, phi_dot, force, force_rate, force_acceleration);
|
||||
ASSERT_TRUE(exact.HasValue());
|
||||
constexpr double h = 1.0e-4;
|
||||
const auto at_time = [&](const double time) {
|
||||
return shell.EvaluateExternalDrillingConstraint(
|
||||
0U, phi + time * phi_dot, phi_dot,
|
||||
force + time * force_rate + 0.5 * time * time * force_acceleration,
|
||||
force_rate + time * force_acceleration, force_acceleration);
|
||||
};
|
||||
const auto plus = at_time(h);
|
||||
const auto minus = at_time(-h);
|
||||
ASSERT_TRUE(plus.HasValue());
|
||||
ASSERT_TRUE(minus.HasValue());
|
||||
EXPECT_NEAR(exact.Value().directional_first,
|
||||
(plus.Value().value - minus.Value().value) / (2.0 * h),
|
||||
2.0e-8);
|
||||
EXPECT_NEAR(exact.Value().directional_second,
|
||||
(plus.Value().value - 2.0 * exact.Value().value +
|
||||
minus.Value().value) /
|
||||
(h * h),
|
||||
3.0e-7);
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
EXPECT_NEAR(exact.Value().gradient_directional_first[row],
|
||||
(plus.Value().gradient[row] - minus.Value().gradient[row]) /
|
||||
(2.0 * h),
|
||||
3.0e-8);
|
||||
EXPECT_NEAR(
|
||||
exact.Value().gradient_directional_second[row],
|
||||
(plus.Value().gradient[row] - 2.0 * exact.Value().gradient[row] +
|
||||
minus.Value().gradient[row]) /
|
||||
(h * h),
|
||||
4.0e-7);
|
||||
}
|
||||
for (std::size_t column = 0U; column < 3U; ++column) {
|
||||
fesa::Vector3 basis{};
|
||||
if (column == 0U) basis = {1.0, 0.0, 0.0};
|
||||
if (column == 1U) basis = {0.0, 1.0, 0.0};
|
||||
if (column == 2U) basis = {0.0, 0.0, 1.0};
|
||||
const auto positive = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, phi + h * basis, phi_dot, force, force_rate, force_acceleration);
|
||||
const auto negative = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, phi - h * basis, phi_dot, force, force_rate, force_acceleration);
|
||||
ASSERT_TRUE(positive.HasValue());
|
||||
ASSERT_TRUE(negative.HasValue());
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
EXPECT_NEAR(exact.Value().hessian(row, column),
|
||||
(positive.Value().gradient[row] -
|
||||
negative.Value().gradient[row]) /
|
||||
(2.0 * h),
|
||||
2.0e-7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual,
|
||||
ExternalDrillingConstraintRejectsInvalidRotationAndHasZeroForceValue) {
|
||||
const auto shell = Shell(Nodes());
|
||||
const auto zero = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, {.2, -.1, .3}, {.1, .2, -.3}, {}, {}, {});
|
||||
ASSERT_TRUE(zero.HasValue());
|
||||
EXPECT_DOUBLE_EQ(zero.Value().value, 0.0);
|
||||
EXPECT_DOUBLE_EQ(zero.Value().directional_first, 0.0);
|
||||
EXPECT_DOUBLE_EQ(zero.Value().directional_second, 0.0);
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
EXPECT_DOUBLE_EQ(zero.Value().gradient[row], 0.0);
|
||||
EXPECT_DOUBLE_EQ(zero.Value().gradient_directional_first[row], 0.0);
|
||||
EXPECT_DOUBLE_EQ(zero.Value().gradient_directional_second[row], 0.0);
|
||||
}
|
||||
const auto invalid = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, {std::acos(-1.0), 0.0, 0.0}, {}, {}, {}, {});
|
||||
EXPECT_FALSE(invalid.HasValue());
|
||||
EXPECT_EQ(invalid.GetStatus().Diagnostics()[0U].code,
|
||||
"invalid-nonlinear-shell-rotation");
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual, TinyRotationPreservesPhysicalForceAndRecoveryEnergy) {
|
||||
for (const bool rotated : {false, true}) {
|
||||
for (const bool warped : {false, true}) {
|
||||
SCOPED_TRACE(rotated);
|
||||
SCOPED_TRACE(warped);
|
||||
auto nodes = Nodes();
|
||||
if (warped) nodes[2U].coordinates[2U] = 0.1;
|
||||
auto frames = Frames();
|
||||
const auto rotation = fesa::ExpSkew(
|
||||
rotated ? fesa::Vector3{0.4, -0.2, 0.7} : fesa::Vector3{});
|
||||
const auto transform = [&](const Coordinates& values) {
|
||||
Coordinates result{};
|
||||
for (std::size_t i = 0U; i < 3U; ++i)
|
||||
for (std::size_t j = 0U; j < 3U; ++j)
|
||||
result[i] += rotation(i, j) * values[j];
|
||||
return result;
|
||||
};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
nodes[node].coordinates = transform(nodes[node].coordinates);
|
||||
frames[node].director = transform(frames[node].director);
|
||||
frames[node].tangent_a = transform(frames[node].tangent_a);
|
||||
frames[node].tangent_b = transform(frames[node].tangent_b);
|
||||
}
|
||||
const fesa::ShellSection section{"Section", 0.2, 0U, {"tiny.inp", 1U}};
|
||||
const fesa::LinearElasticMaterial material{
|
||||
"Material", 120.0, 0.25, {"tiny.inp", 2U}};
|
||||
const auto shell = fesa::NonlinearMitc4Shell::Create(
|
||||
NodePointers(nodes), frames, section, material);
|
||||
ASSERT_TRUE(shell.HasValue());
|
||||
fesa::NonlinearMitc4TrialDofs tiny{};
|
||||
fesa::Vector increment{24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
tiny.rotation_vector[node] =
|
||||
1e-17 * fesa::Vector3{frames[node].tangent_a};
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
increment[6U * node + 3U + c] = tiny.rotation_vector[node][c];
|
||||
}
|
||||
const auto zero =
|
||||
fesa::EvaluateInternalResidualAndTangent(shell.Value(), {});
|
||||
const auto actual =
|
||||
fesa::EvaluateInternalResidualAndTangent(shell.Value(), tiny);
|
||||
ASSERT_TRUE(zero.HasValue());
|
||||
ASSERT_TRUE(actual.HasValue());
|
||||
EXPECT_DOUBLE_EQ(zero.Value().physical_strain_energy, 0.0);
|
||||
double error = 0.0;
|
||||
double scale = 0.0;
|
||||
double quadratic_energy = 0.0;
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
EXPECT_DOUBLE_EQ(zero.Value().physical_force[row], 0.0);
|
||||
double expected = 0.0;
|
||||
for (std::size_t column = 0U; column < 24U; ++column)
|
||||
expected += (zero.Value().internal_tangent(row, column) -
|
||||
zero.Value().drilling_tangent(row, column)) *
|
||||
increment[column];
|
||||
error = std::max(error,
|
||||
std::abs(actual.Value().physical_force[row] - expected));
|
||||
scale = std::max(scale, std::abs(expected));
|
||||
quadratic_energy += 0.5 * increment[row] * expected;
|
||||
}
|
||||
ASSERT_GT(scale, 0.0);
|
||||
ASSERT_GT(quadratic_energy, 0.0);
|
||||
EXPECT_LE(error / scale, 1e-12);
|
||||
EXPECT_GT(actual.Value().physical_strain_energy, 0.0);
|
||||
EXPECT_LE(std::abs(actual.Value().physical_strain_energy - quadratic_energy) /
|
||||
quadratic_energy,
|
||||
1e-12);
|
||||
if (!warped) {
|
||||
// Uniform director tilt has .5 * (5/6) * G * t * area * theta^2.
|
||||
constexpr double shear_energy = 1.6e-33;
|
||||
EXPECT_LE(std::abs(actual.Value().physical_strain_energy - shear_energy) /
|
||||
shear_energy,
|
||||
1e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user