feat: implement consistent-mass HHT shell dynamics
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "fesa/elements/nonlinear_mitc4_inertia.h"
|
||||
#include "fesa/elements/nonlinear_mitc4_shell.h"
|
||||
#include "fesa/model/model_types.h"
|
||||
|
||||
@@ -417,4 +418,165 @@ TEST(NonlinearMitc4Residual, RejectsNonfiniteTrialData) {
|
||||
EXPECT_FALSE(overflow_result.HasValue());
|
||||
}
|
||||
|
||||
void CheckThirdSweep(const fesa::NonlinearMitc4TrialDofs& trial) {
|
||||
const auto nodes = Nodes();
|
||||
const auto shell = Shell(nodes);
|
||||
auto direction = Direction();
|
||||
double norm = 0.0;
|
||||
const auto unscaled = ToVector(direction);
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
norm = std::hypot(norm, unscaled[i]);
|
||||
}
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
direction.displacement[node] = 2.0 * direction.displacement[node] / norm;
|
||||
direction.rotation_vector[node] = direction.rotation_vector[node] / norm;
|
||||
}
|
||||
const auto velocity = ToVector(direction);
|
||||
const auto exact =
|
||||
fesa::EvaluateInternalResidualAndTangent(shell, trial, &velocity);
|
||||
ASSERT_TRUE(exact.HasValue());
|
||||
unsigned int consecutive = 0U;
|
||||
bool passed = false;
|
||||
for (int power = 8; power <= 20; power += 2) {
|
||||
const double h = std::ldexp(1.0, -power);
|
||||
const auto plus = fesa::EvaluateInternalResidualAndTangent(
|
||||
shell, AddScaled(trial, direction, h));
|
||||
const auto minus = fesa::EvaluateInternalResidualAndTangent(
|
||||
shell, AddScaled(trial, direction, -h));
|
||||
ASSERT_TRUE(plus.HasValue());
|
||||
ASSERT_TRUE(minus.HasValue());
|
||||
double error = 0.0;
|
||||
double analytic_norm = 0.0;
|
||||
double fd_norm = 0.0;
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
double fd = 0.0;
|
||||
for (std::size_t j = 0U; j < 24U; ++j) {
|
||||
fd += (plus.Value().internal_tangent(i, j) -
|
||||
minus.Value().internal_tangent(i, j)) *
|
||||
velocity[j] / (2.0 * h);
|
||||
}
|
||||
const double d = i % 6U < 3U ? 2.0 : 1.0;
|
||||
const double analytic = exact.Value().force_directional_second[i];
|
||||
error = std::hypot(error, d * (analytic - fd));
|
||||
analytic_norm = std::hypot(analytic_norm, d * analytic);
|
||||
fd_norm = std::hypot(fd_norm, d * fd);
|
||||
}
|
||||
if (power >= 10 && power <= 18 &&
|
||||
error / (analytic_norm + fd_norm) <= 1e-6) {
|
||||
++consecutive;
|
||||
passed = passed || consecutive >= 2U;
|
||||
} else {
|
||||
consecutive = 0U;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(passed);
|
||||
EXPECT_GE(exact.Value().drilling_energy, 0.0);
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
EXPECT_DOUBLE_EQ(
|
||||
exact.Value().internal_force[i],
|
||||
exact.Value().physical_force[i] + exact.Value().drilling_force[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual,
|
||||
ZeroTrialOnTranslatedDecimalMeshHasNoArtificialForce) {
|
||||
auto nodes = Nodes();
|
||||
nodes[0].coordinates = {3.84615385, 2.30769231, 0.0};
|
||||
nodes[1].coordinates = {4.23076923, 2.30769231, 0.0};
|
||||
nodes[2].coordinates = {4.23076923, 2.69230769, 0.0};
|
||||
nodes[3].coordinates = {3.84615385, 2.69230769, 0.0};
|
||||
const auto shell = Shell(nodes);
|
||||
const auto evaluated = fesa::EvaluateInternalResidualAndTangent(shell, {});
|
||||
ASSERT_TRUE(evaluated.HasValue());
|
||||
for (std::size_t i = 0; i < 24U; ++i)
|
||||
EXPECT_DOUBLE_EQ(evaluated.Value().internal_force[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(evaluated.Value().drilling_energy, 0.0);
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual, ExactDirectionalThirdGradientMatchesTangentSweep) {
|
||||
CheckThirdSweep(Trial());
|
||||
}
|
||||
|
||||
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}) {
|
||||
SCOPED_TRACE(angle);
|
||||
auto trial = Trial();
|
||||
for (auto& phi : trial.rotation_vector) phi = {angle, 0., 0.};
|
||||
CheckThirdSweep(trial);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual,
|
||||
ExactDrillingConstraintHasIndependentForceGradientAndHessian) {
|
||||
for (bool warped : {false, true}) {
|
||||
auto nodes = Nodes();
|
||||
if (warped) nodes[2U].coordinates[2U] = 0.1;
|
||||
const auto shell = Shell(nodes);
|
||||
auto trial = Trial();
|
||||
trial.rotation_vector[0U] = {.6, -.2, .1};
|
||||
const auto direction = Direction();
|
||||
const auto velocity = ToVector(direction);
|
||||
const auto exact =
|
||||
fesa::EvaluateInternalResidualAndTangent(shell, trial, &velocity);
|
||||
ASSERT_TRUE(exact.HasValue());
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto rotation = fesa::EvaluateDynamicRotation(
|
||||
trial.rotation_vector[node], {0., 0., 1.},
|
||||
direction.rotation_vector[node], {});
|
||||
ASSERT_TRUE(rotation.HasValue());
|
||||
double psi = 0.;
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
psi += rotation.Value().drill_null[c] *
|
||||
exact.Value().internal_force[6U * node + 3U + c];
|
||||
EXPECT_NEAR(exact.Value().drilling_constraint[node], psi, 1e-11);
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
double gradient = 0.;
|
||||
for (std::size_t c = 0U; c < 3U; ++c) {
|
||||
gradient += rotation.Value().drill_null[c] *
|
||||
exact.Value().internal_tangent(6U * node + 3U + c, i);
|
||||
if (i / 6U == node && i % 6U >= 3U)
|
||||
gradient += rotation.Value().null_gradient(c, i % 6U - 3U) *
|
||||
exact.Value().internal_force[6U * node + 3U + c];
|
||||
}
|
||||
EXPECT_NEAR(exact.Value().drilling_constraint_gradient(node, i),
|
||||
gradient, 1e-10);
|
||||
}
|
||||
}
|
||||
unsigned consecutive = 0U;
|
||||
bool passed = false;
|
||||
for (int power = 8; power <= 20; power += 2) {
|
||||
const double h = std::ldexp(1., -power);
|
||||
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());
|
||||
double error = 0., anorm = 0., fnorm = 0.;
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
double analytic = 0.;
|
||||
for (std::size_t j = 0U; j < 24U; ++j)
|
||||
analytic += exact.Value().drilling_constraint_hessian[node](i, j) *
|
||||
velocity[j];
|
||||
const double fd =
|
||||
(plus.Value().drilling_constraint_gradient(node, i) -
|
||||
minus.Value().drilling_constraint_gradient(node, i)) /
|
||||
(2 * h);
|
||||
const double scale = i % 6U < 3U ? 2. : 1.;
|
||||
error = std::hypot(error, scale * (analytic - fd));
|
||||
anorm = std::hypot(anorm, scale * analytic);
|
||||
fnorm = std::hypot(fnorm, scale * fd);
|
||||
}
|
||||
if (power >= 10 && power <= 18 && error / (anorm + fnorm) <= 1e-6) {
|
||||
++consecutive;
|
||||
passed = passed || consecutive >= 2U;
|
||||
} else
|
||||
consecutive = 0U;
|
||||
}
|
||||
EXPECT_TRUE(passed);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user