Add immutable MITC4 drilling selection and direct director springs

This commit is contained in:
KOKO\Mimi
2026-10-20 08:52:43 +09:00
parent 52e321e15c
commit 8bac3ebf49
9 changed files with 670 additions and 22 deletions
@@ -133,6 +133,7 @@ TEST(ElementCapabilities, ShellTrialPreservesCompleteDirectionalResponseAndFailu
Same(a.drilling_metric, b.drilling_metric);
Same(a.drilling_energy, b.drilling_energy);
Same(a.force_directional_second, b.force_directional_second);
Same(a.drilling_force_directional_second, b.drilling_force_directional_second);
Same(a.drilling_constraint, b.drilling_constraint);
Same(a.drilling_constraint_gradient, b.drilling_constraint_gradient);
for (std::size_t n = 0; n < 4; ++n) {
@@ -6,6 +6,7 @@
#include <cstddef>
#include <cstdint>
#include <limits>
#include <cstring>
#include <stdexcept>
#include <string>
@@ -13,6 +14,8 @@
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include "fesa/model/model_types.h"
#include "elements/mitc4_drilling_internal.h"
#include "elements/nonlinear_mitc4_configuration_internal.h"
namespace {
@@ -44,7 +47,7 @@ std::array<fesa::ShellNodeInitialFrame, 4> Frames() {
{3U, {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}}}};
}
fesa::NonlinearMitc4Shell Shell(const std::array<fesa::Node, 4>& nodes) {
fesa::NonlinearMitc4Shell ProductionShell(const std::array<fesa::Node, 4>& nodes) {
const fesa::ShellSection section{
"Section", 0.2, 0U, {"nonlinear-mitc4-tangent.inp", 20U}};
const fesa::LinearElasticMaterial material{
@@ -57,6 +60,11 @@ fesa::NonlinearMitc4Shell Shell(const std::array<fesa::Node, 4>& nodes) {
return shell.Value();
}
fesa::NonlinearMitc4Shell LegacyShell(const std::array<fesa::Node, 4>& nodes) {
return fesa::mitc4_drilling_internal::TestAccess::WithMode(
ProductionShell(nodes), fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
}
fesa::NonlinearMitc4TrialDofs Trial() {
fesa::NonlinearMitc4TrialDofs trial{};
trial.displacement[0U] = {-0.02, 0.01, 0.03};
@@ -200,7 +208,7 @@ void ExpectRelative(const double actual, const double expected) {
// NSNR-ELEM-TAN-001
TEST(NonlinearMitc4Residual, EvaluatesResidualTangentAndRecoveryAtOneTrial) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
const auto shell = LegacyShell(nodes);
const auto trial = Trial();
const auto recovery = shell.RecoverState(trial);
const auto result = fesa::EvaluateInternalResidualAndTangent(shell, trial);
@@ -230,7 +238,7 @@ TEST(NonlinearMitc4Residual, EvaluatesResidualTangentAndRecoveryAtOneTrial) {
// NSNR-ELEM-TAN-002 and NSNR-ELEM-TAN-003
TEST(NonlinearMitc4Tangent, MatchesTheApprovedDirectionalSchedule) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
const auto shell = LegacyShell(nodes);
const auto trial = Trial();
const auto direction = WorkConjugateDirection(nodes);
const double length = ReferenceLength(nodes);
@@ -295,7 +303,7 @@ TEST(NonlinearMitc4Tangent, MatchesTheApprovedDirectionalSchedule) {
TEST(NonlinearMitc4Tangent, PreservesRigidTranslationNullspaceAndSymmetry) {
const auto nodes = Nodes();
const auto equilibrium =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), {});
fesa::EvaluateInternalResidualAndTangent(LegacyShell(nodes), {});
ASSERT_TRUE(equilibrium.HasValue());
const double length = ReferenceLength(nodes);
@@ -326,9 +334,9 @@ TEST(NonlinearMitc4Tangent, PreservesRigidTranslationNullspaceAndSymmetry) {
TEST(NonlinearMitc4Residual, DoesNotAcceptExternalLoadShares) {
const auto nodes = Nodes();
const auto first =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), Trial());
fesa::EvaluateInternalResidualAndTangent(LegacyShell(nodes), Trial());
const auto second =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), Trial());
fesa::EvaluateInternalResidualAndTangent(LegacyShell(nodes), Trial());
ASSERT_TRUE(first.HasValue());
ASSERT_TRUE(second.HasValue());
for (std::size_t index = 0U; index < 24U; ++index) {
@@ -357,7 +365,7 @@ TEST(NonlinearMitc4Objectivity, FiniteRigidMotionHasZeroInternalForce) {
trial.rotation_vector[node] = rotation;
}
const auto result =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), trial);
fesa::EvaluateInternalResidualAndTangent(LegacyShell(nodes), trial);
ASSERT_TRUE(result.HasValue());
EXPECT_LE(result.Value().internal_residual.Norm(), kNumericalTolerance);
EXPECT_LE(result.Value().physical_strain_energy, kNumericalTolerance);
@@ -366,7 +374,7 @@ TEST(NonlinearMitc4Objectivity, FiniteRigidMotionHasZeroInternalForce) {
// NSNR-ELEM-TAN-008
TEST(NonlinearMitc4Patch, RetainsPhysicalRecoveryIdentities) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
const auto shell = LegacyShell(nodes);
constexpr double kMembraneStrain = 1.0e-2;
const double green_strain =
kMembraneStrain + 0.5 * kMembraneStrain * kMembraneStrain;
@@ -409,7 +417,7 @@ TEST(NonlinearMitc4Residual, RejectsNonfiniteTrialData) {
trial.rotation_vector[2U] = {std::numeric_limits<double>::quiet_NaN(), 0.0,
0.0};
const auto result =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), trial);
fesa::EvaluateInternalResidualAndTangent(LegacyShell(nodes), trial);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
"invalid-nonlinear-shell-trial");
@@ -417,13 +425,13 @@ TEST(NonlinearMitc4Residual, RejectsNonfiniteTrialData) {
auto overflow = Trial();
overflow.displacement[1U] = {1.0e300, 0.0, 0.0};
const auto overflow_result =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), overflow);
fesa::EvaluateInternalResidualAndTangent(LegacyShell(nodes), overflow);
EXPECT_FALSE(overflow_result.HasValue());
}
void CheckThirdSweep(const fesa::NonlinearMitc4TrialDofs& trial) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
const auto shell = LegacyShell(nodes);
auto direction = Direction();
double norm = 0.0;
const auto unscaled = ToVector(direction);
@@ -488,7 +496,7 @@ TEST(NonlinearMitc4Residual,
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 shell = LegacyShell(nodes);
const auto evaluated = fesa::EvaluateInternalResidualAndTangent(shell, {});
ASSERT_TRUE(evaluated.HasValue());
for (std::size_t i = 0; i < 24U; ++i)
@@ -566,7 +574,7 @@ TEST(NonlinearMitc4Residual,
for (bool warped : {false, true}) {
auto nodes = Nodes();
if (warped) nodes[2U].coordinates[2U] = 0.1;
const auto shell = Shell(nodes);
const auto shell = LegacyShell(nodes);
auto trial = Trial();
trial.rotation_vector[0U] = {.6, -.2, .1};
const auto direction = Direction();
@@ -636,7 +644,7 @@ TEST(NonlinearMitc4Residual,
TEST(NonlinearMitc4Residual,
ExposesExactDrillingConstraintThirdDirectionalGradient) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
const auto shell = LegacyShell(nodes);
const auto trial = Trial();
const auto direction = Direction();
const auto velocity = ToVector(direction);
@@ -679,7 +687,7 @@ TEST(NonlinearMitc4Residual,
TEST(NonlinearMitc4Residual,
ExternalDrillingConstraintIncludesForceTimeAndCurvatureTerms) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
const auto shell = LegacyShell(nodes);
const fesa::Vector3 phi{.6, -.2, .1};
const fesa::Vector3 phi_dot{.2, -.1, .3};
const fesa::Vector3 force{1.7, -.8, .4};
@@ -742,7 +750,7 @@ TEST(NonlinearMitc4Residual,
TEST(NonlinearMitc4Residual,
ExternalDrillingConstraintRejectsInvalidRotationAndHasZeroForceValue) {
const auto shell = Shell(Nodes());
const auto shell = LegacyShell(Nodes());
const auto zero = shell.EvaluateExternalDrillingConstraint(
0U, {.2, -.1, .3}, {.1, .2, -.3}, {}, {}, {});
ASSERT_TRUE(zero.HasValue());
@@ -838,4 +846,470 @@ TEST(NonlinearMitc4Residual, TinyRotationPreservesPhysicalForceAndRecoveryEnergy
}
}
// Independent fixed-initial-director spring oracle: a total-chart rigid spin
// carries numerical energy even though the physical shell is undeformed.
TEST(Mitc4DrillingSelection, ProductionUsesDirectInitialDirectorSpring) {
const auto nodes = Nodes();
const auto shell = ProductionShell(nodes);
const fesa::ShellSection section{"Section", 0.2, 0U, {"spring.inp", 1U}};
const fesa::LinearElasticMaterial material{
"Material", 120.0, 0.25, {"spring.inp", 2U}};
const auto linear = fesa::Mitc4Shell::Create(
NodePointers(nodes),
{{{0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}}}, section,
material);
ASSERT_TRUE(linear.HasValue());
const auto stiffness = linear.Value().Stiffness();
ASSERT_TRUE(stiffness.HasValue());
double rotary_trace = 0.0;
for (std::size_t node = 0; node < 4; ++node)
for (std::size_t c = 3; c < 5; ++c)
rotary_trace +=
stiffness.Value().physical_local20(5 * node + c, 5 * node + c);
const double kd = 1e-3 * rotary_trace / 8.;
fesa::NonlinearMitc4TrialDofs trial{};
const fesa::Vector3 phi{0., 0., 0.4};
const auto rotation = fesa::ExpSkew(phi);
for (std::size_t node = 0; node < 4; ++node) {
const fesa::Vector3 x{nodes[node].coordinates};
trial.rotation_vector[node] = phi;
fesa::Vector3 rotated{
rotation(0, 0) * x[0] + rotation(0, 1) * x[1] + rotation(0, 2) * x[2],
rotation(1, 0) * x[0] + rotation(1, 1) * x[1] + rotation(1, 2) * x[2],
rotation(2, 0) * x[0] + rotation(2, 1) * x[1] + rotation(2, 2) * x[2]};
trial.displacement[node] = rotated - x;
}
const auto actual = fesa::EvaluateInternalResidualAndTangent(shell, trial);
ASSERT_TRUE(actual.HasValue());
EXPECT_NEAR(actual.Value().physical_strain_energy, 0., 1e-25);
EXPECT_NEAR(actual.Value().drilling_energy, .5 * kd * 4 * .4 * .4, 1e-14);
for (std::size_t row = 0; row < 24; ++row) {
const double force = row % 6 == 5 ? kd * .4 : 0.;
EXPECT_NEAR(actual.Value().drilling_force[row], force, 1e-14);
for (std::size_t col = 0; col < 24; ++col) {
const double tangent = row == col && row % 6 == 5 ? kd : 0.;
EXPECT_NEAR(actual.Value().drilling_tangent(row, col), tangent, 1e-14);
}
}
}
TEST(Mitc4DrillingSelection,
DirectSpringUsesEachInitialDirectorAndConstantBlocks) {
for (const bool warped : {false, true}) {
auto nodes = Nodes();
if (warped) nodes[2].coordinates[2] = .1;
auto frames = Frames();
const auto transform = [](const fesa::Matrix& r, const Coordinates& x) {
Coordinates y{};
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) y[i] += r(i, j) * x[j];
return y;
};
const auto global = fesa::ExpSkew({.4, -.3, .2});
std::array<Coordinates, 4> directors{};
for (std::size_t n = 0; n < 4; ++n) {
const auto r =
fesa::ExpSkew({0., warped ? .03 * static_cast<double>(n) : 0., 0.});
frames[n].director = transform(global, transform(r, frames[n].director));
frames[n].tangent_a =
transform(global, transform(r, frames[n].tangent_a));
frames[n].tangent_b =
transform(global, transform(r, frames[n].tangent_b));
nodes[n].coordinates = transform(global, nodes[n].coordinates);
directors[n] = frames[n].director;
}
const fesa::ShellSection section{"Section", .2, 0, {"spring.inp", 1}};
const fesa::LinearElasticMaterial material{
"Material", 120., .25, {"spring.inp", 2}};
const auto shell = fesa::NonlinearMitc4Shell::Create(
NodePointers(nodes), frames, section, material);
const auto linear = fesa::Mitc4Shell::Create(NodePointers(nodes), directors,
section, material);
ASSERT_TRUE(shell.HasValue());
ASSERT_TRUE(linear.HasValue());
const auto k = linear.Value().Stiffness();
ASSERT_TRUE(k.HasValue());
double trace = 0.;
for (std::size_t n = 0; n < 4; ++n)
for (std::size_t c = 3; c < 5; ++c)
trace += k.Value().physical_local20(5 * n + c, 5 * n + c);
const double kd = 1e-3 * trace / 8.;
auto trial = Trial();
trial.rotation_vector[0] = {.65, -.4, .3};
const auto direction = ToVector(Direction());
const auto response = fesa::EvaluateInternalResidualAndTangent(
shell.Value(), trial, &direction);
const auto shifted = fesa::EvaluateInternalResidualAndTangent(
shell.Value(), AddScaled(trial, Direction(), .2), &direction);
ASSERT_TRUE(response.HasValue());
ASSERT_TRUE(shifted.HasValue());
double energy = 0.;
for (std::size_t n = 0; n < 4; ++n) {
const double gamma =
fesa::Vector3{directors[n]}.Dot(trial.rotation_vector[n]);
energy += .5 * kd * gamma * gamma;
const fesa::Vector3 d{directors[n]};
const auto rotation = fesa::EvaluateDynamicRotation(
trial.rotation_vector[n], d, Direction().rotation_vector[n], {});
ASSERT_TRUE(rotation.HasValue());
EXPECT_NEAR(response.Value().drilling_constraint[n],
kd * gamma * rotation.Value().drill_null.Dot(d), 1e-14);
for (std::size_t c = 0; c < 3; ++c) {
double ds = 0.;
for (std::size_t j = 0; j < 3; ++j)
ds += d[j] * rotation.Value().null_gradient(j, c);
EXPECT_NEAR(
response.Value().drilling_constraint_gradient(n, 6 * n + 3 + c),
kd * (d[c] * rotation.Value().drill_null.Dot(d) + gamma * ds),
1e-14);
}
for (std::size_t c = 0; c < 3; ++c)
EXPECT_NEAR(response.Value().drilling_force[6 * n + 3 + c],
kd * gamma * directors[n][c], 1e-14);
}
EXPECT_NEAR(response.Value().drilling_energy, energy, 1e-14);
for (std::size_t i = 0; i < 24; ++i) {
EXPECT_DOUBLE_EQ(response.Value().drilling_force_directional_second[i],
0.);
if (i % 6 < 3) EXPECT_DOUBLE_EQ(response.Value().drilling_force[i], 0.);
for (std::size_t j = 0; j < 24; ++j) {
const double expected =
i / 6 == j / 6 && i % 6 >= 3 && j % 6 >= 3
? kd * directors[i / 6][i % 6 - 3] * directors[i / 6][j % 6 - 3]
: 0.;
EXPECT_NEAR(response.Value().drilling_tangent(i, j), expected, 1e-14);
EXPECT_NEAR(response.Value().drilling_metric(i, j), expected, 1e-14);
EXPECT_DOUBLE_EQ(response.Value().drilling_tangent(i, j),
shifted.Value().drilling_tangent(i, j));
if (expected == 0.)
EXPECT_DOUBLE_EQ(response.Value().drilling_tangent(i, j), 0.);
}
}
}
}
TEST(Mitc4DrillingSelection,
DirectConstraintUsesPhysicalNullAndItsHighDerivatives) {
const auto nodes = Nodes();
const auto shell = ProductionShell(nodes);
auto trial = Trial();
trial.rotation_vector[0] = {.65, -.4, .3};
const auto direction = Direction();
const auto velocity = ToVector(direction);
const auto response =
fesa::EvaluateInternalResidualAndTangent(shell, trial, &velocity);
ASSERT_TRUE(response.HasValue());
const auto zero =
fesa::EvaluateInternalResidualAndTangent(shell, {}, &velocity);
ASSERT_TRUE(zero.HasValue());
const double kd = zero.Value().drilling_tangent(5, 5);
// Independent inertia API provides n, Dn and D2n, never a twist angle.
const fesa::Vector3 d{0., 0., 1.};
const auto scalar_gradient = [&](const fesa::Vector3& phi, std::size_t c) {
const auto r = fesa::EvaluateDynamicRotation(phi, d, {}, {});
EXPECT_TRUE(r.HasValue());
double projection_derivative = 0.;
for (std::size_t k = 0; k < 3; ++k)
projection_derivative += r.Value().null_gradient(k, c) * d[k];
return kd * (d[c] * r.Value().drill_null.Dot(d) +
d.Dot(phi) * projection_derivative);
};
constexpr double h = .002;
for (std::size_t n = 0; n < 4; ++n) {
const auto phi = trial.rotation_vector[n];
const auto v = direction.rotation_vector[n];
const auto r = fesa::EvaluateDynamicRotation(phi, d, v, {});
ASSERT_TRUE(r.HasValue());
const double gamma = d.Dot(phi);
const double gamma_dot = d.Dot(v);
double projection_dot = 0.;
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j)
projection_dot += d[i] * r.Value().null_gradient(i, j) * v[j];
const double expected_second =
kd * (2. * gamma_dot * projection_dot +
gamma * r.Value().null_directional_second.Dot(d));
double actual_second = 0.;
EXPECT_NEAR(response.Value().drilling_constraint[n],
kd * gamma * r.Value().drill_null.Dot(d), 1e-14);
for (std::size_t i = 0; i < 24; ++i) {
const double gradient =
i / 6 == n && i % 6 >= 3 ? scalar_gradient(phi, i % 6 - 3) : 0.;
EXPECT_NEAR(response.Value().drilling_constraint_gradient(n, i), gradient,
1e-14);
for (std::size_t j = 0; j < 24; ++j)
actual_second += velocity[i] *
response.Value().drilling_constraint_hessian[n](i, j) *
velocity[j];
if (i / 6 == n && i % 6 >= 3) {
const auto c = i % 6 - 3;
const double fd =
(scalar_gradient(phi + h * v, c) - 2. * scalar_gradient(phi, c) +
scalar_gradient(phi - h * v, c)) /
(h * h);
const double actual =
response.Value()
.drilling_constraint_third_directional_gradient[n][i];
EXPECT_NEAR(actual, fd,
2e-5 * (std::abs(actual) + std::abs(fd) + kd * 1e-4));
} else {
EXPECT_DOUBLE_EQ(
response.Value()
.drilling_constraint_third_directional_gradient[n][i],
0.);
}
}
EXPECT_NEAR(actual_second, expected_second, 1e-14);
}
}
TEST(Mitc4DrillingSelection, DirectExternalConstraintUsesActualInertiaNull) {
const auto nodes = Nodes();
const auto shell = ProductionShell(nodes);
const fesa::Vector3 phi{.65, -.4, .3}, velocity{.2, .1, -.15};
const fesa::Vector3 force{.7, -.3, .2}, rate{-.1, .2, .4},
acceleration{.5, -.2, .1};
const auto actual = shell.EvaluateExternalDrillingConstraint(
0, phi, velocity, force, rate, acceleration);
const auto rotation =
fesa::EvaluateDynamicRotation(phi, {0., 0., 1.}, velocity, {});
ASSERT_TRUE(actual.HasValue());
ASSERT_TRUE(rotation.HasValue());
const double h = .002;
const auto plus = shell.EvaluateExternalDrillingConstraint(
0, phi + h * velocity, velocity,
force + h * rate + .5 * h * h * acceleration, rate + h * acceleration,
acceleration);
const auto minus = shell.EvaluateExternalDrillingConstraint(
0, phi - h * velocity, velocity,
force - h * rate + .5 * h * h * acceleration, rate - h * acceleration,
acceleration);
ASSERT_TRUE(plus.HasValue());
ASSERT_TRUE(minus.HasValue());
for (std::size_t c = 0; c < 3; ++c) {
EXPECT_NEAR(
actual.Value().gradient_directional_first[c],
(plus.Value().gradient[c] - minus.Value().gradient[c]) / (2. * h),
1e-7);
EXPECT_NEAR(actual.Value().gradient_directional_second[c],
(plus.Value().gradient[c] - 2. * actual.Value().gradient[c] +
minus.Value().gradient[c]) /
(h * h),
2e-7);
std::array<double, 3> basis{};
basis[c] = 1.;
constexpr double coordinate_h = 1e-4;
const auto positive = shell.EvaluateExternalDrillingConstraint(
0, phi + coordinate_h * fesa::Vector3{basis}, velocity, force, rate,
acceleration);
const auto negative = shell.EvaluateExternalDrillingConstraint(
0, phi - coordinate_h * fesa::Vector3{basis}, velocity, force, rate,
acceleration);
ASSERT_TRUE(positive.HasValue());
ASSERT_TRUE(negative.HasValue());
for (std::size_t j = 0; j < 3; ++j)
EXPECT_NEAR(
actual.Value().hessian(j, c),
(positive.Value().gradient[j] - negative.Value().gradient[j]) /
(2. * coordinate_h),
1e-7);
}
const auto& n = rotation.Value();
EXPECT_NEAR(actual.Value().value, -n.drill_null.Dot(force), 1e-14);
fesa::Vector3 n_dot{};
std::array<double, 3> values{};
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j)
values[i] += n.null_gradient(i, j) * velocity[j];
n_dot = fesa::Vector3{values};
EXPECT_NEAR(actual.Value().directional_first,
-n_dot.Dot(force) - n.drill_null.Dot(rate), 1e-14);
EXPECT_NEAR(actual.Value().directional_second,
-n.null_directional_second.Dot(force) - 2. * n_dot.Dot(rate) -
n.drill_null.Dot(acceleration),
1e-14);
for (std::size_t c = 0; c < 3; ++c) {
double expected = 0.;
for (std::size_t i = 0; i < 3; ++i)
expected -= n.null_gradient(i, c) * force[i];
EXPECT_NEAR(actual.Value().gradient[c], expected, 1e-14);
}
}
TEST(Mitc4DrillingSelection,
ModesAreImmutableReferenceInputsAndPreservePhysicalResponse) {
const auto nodes = Nodes();
const auto direct = ProductionShell(nodes);
const auto legacy = LegacyShell(nodes);
EXPECT_EQ(direct.DrillingMode(), fesa::Mitc4DrillingMode::kDirectSpring);
EXPECT_EQ(legacy.DrillingMode(),
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
EXPECT_STREQ(fesa::Mitc4DrillingModeName(direct.DrillingMode()),
"direct-initial-director-spring");
EXPECT_STREQ(fesa::Mitc4DrillingModeName(legacy.DrillingMode()),
"legacy-corotated-twist");
const auto a =
fesa::nonlinear_mitc4_internal::ConfigurationSnapshot::Read(direct);
const auto b =
fesa::nonlinear_mitc4_internal::ConfigurationSnapshot::Read(legacy);
EXPECT_EQ(a.values, b.values);
EXPECT_NE(a.drilling_mode, b.drilling_mode);
const auto direction = ToVector(Direction());
const auto r =
fesa::EvaluateInternalResidualAndTangent(direct, Trial(), &direction);
const auto l =
fesa::EvaluateInternalResidualAndTangent(legacy, Trial(), &direction);
ASSERT_TRUE(r.HasValue());
ASSERT_TRUE(l.HasValue());
EXPECT_DOUBLE_EQ(r.Value().physical_strain_energy,
l.Value().physical_strain_energy);
EXPECT_NE(r.Value().drilling_energy, l.Value().drilling_energy);
for (std::size_t i = 0; i < 24; ++i) {
EXPECT_DOUBLE_EQ(r.Value().physical_force[i], l.Value().physical_force[i]);
for (std::size_t j = 0; j < 24; ++j)
EXPECT_NEAR(
r.Value().internal_tangent(i, j) - r.Value().drilling_tangent(i, j),
l.Value().internal_tangent(i, j) - l.Value().drilling_tangent(i, j),
1e-14);
}
for (std::size_t gp = 0; gp < 4; ++gp)
EXPECT_EQ(r.Value().recovery.gauss_points[gp].section_resultant,
l.Value().recovery.gauss_points[gp].section_resultant);
const fesa::ShellSection section{"Section", .2, 0, {"spring.inp", 1}};
const fesa::LinearElasticMaterial material{
"Material", 120., .25, {"spring.inp", 2}};
const auto linear = fesa::Mitc4Shell::Create(
NodePointers(nodes),
{{{0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}}}, section,
material);
ASSERT_TRUE(linear.HasValue());
EXPECT_EQ(linear.Value().DrillingMode(), direct.DrillingMode());
const auto alternate = fesa::mitc4_drilling_internal::TestAccess::WithMode(
linear.Value(), fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
const auto k1 = linear.Value().Stiffness();
const auto k2 = alternate.Stiffness();
ASSERT_TRUE(k1.HasValue());
ASSERT_TRUE(k2.HasValue());
for (std::size_t i = 0; i < 24; ++i)
for (std::size_t j = 0; j < 24; ++j)
EXPECT_DOUBLE_EQ(k1.Value().stabilized_global24(i, j),
k2.Value().stabilized_global24(i, j));
}
TEST(Mitc4DrillingSelection, DirectRejectsInvalidScaleAndNonfiniteTrial) {
const auto nodes = Nodes();
const fesa::ShellSection section{"Section", .2, 0, {"spring.inp", 1}};
for (double youngs_modulus : {1e-320, (std::numeric_limits<double>::max)()}) {
const fesa::LinearElasticMaterial material{
"Material", youngs_modulus, .25, {"spring.inp", 2}};
const auto invalid = fesa::NonlinearMitc4Shell::Create(
NodePointers(nodes), Frames(), section, material);
ASSERT_FALSE(invalid.HasValue());
EXPECT_EQ(invalid.GetStatus().Diagnostics()[0].code,
youngs_modulus < 1.0 ? "invalid-nonlinear-shell-drilling"
: "invalid-shell-material");
}
const auto shell = ProductionShell(nodes);
for (double value : {std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity()}) {
auto trial = Trial();
trial.rotation_vector[0] = {value, 0., 0.};
EXPECT_FALSE(
fesa::EvaluateInternalResidualAndTangent(shell, trial).HasValue());
}
const auto valid = fesa::EvaluateInternalResidualAndTangent(shell, {});
ASSERT_TRUE(valid.HasValue());
EXPECT_DOUBLE_EQ(valid.Value().drilling_energy, 0.);
}
TEST(Mitc4DrillingSelection, DirectRetainsPositiveSubnormalSpringScale) {
const auto nodes = Nodes();
const fesa::ShellSection section{"Section", .2, 0, {"spring.inp", 1}};
unsigned accepted = 0;
for (double youngs_modulus :
{1e-315, 1e-316, 1e-317, 1e-318, 2e-318, 3e-318, 1e-319}) {
const fesa::LinearElasticMaterial material{
"Material", youngs_modulus, .25, {"spring.inp", 2}};
const auto linear = fesa::Mitc4Shell::Create(
NodePointers(nodes),
{{{0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}}}, section,
material);
ASSERT_TRUE(linear.HasValue());
const auto stiffness = linear.Value().Stiffness();
const auto shell = fesa::NonlinearMitc4Shell::Create(
NodePointers(nodes), Frames(), section, material);
if (!stiffness.HasValue()) {
EXPECT_FALSE(shell.HasValue());
continue;
}
ASSERT_TRUE(shell.HasValue());
++accepted;
fesa::NonlinearMitc4TrialDofs trial{};
trial.rotation_vector[0] = {0., 0., 1.};
const auto actual =
fesa::EvaluateInternalResidualAndTangent(shell.Value(), trial);
ASSERT_TRUE(actual.HasValue());
// Scale is already validated. No later half-scale rounding may change K.
const double kd = stiffness.Value().drilling_stiffness;
EXPECT_EQ(actual.Value().drilling_tangent(5, 5), kd);
EXPECT_EQ(actual.Value().drilling_force[5], kd);
trial.rotation_vector[0] = {0., 0., 2.};
const auto double_angle =
fesa::EvaluateInternalResidualAndTangent(shell.Value(), trial);
ASSERT_TRUE(double_angle.HasValue());
// U(2) = 2*kd is representable even for the least positive spring scale.
EXPECT_EQ(double_angle.Value().drilling_energy, 2. * kd);
EXPECT_EQ(double_angle.Value().drilling_force[5], 2. * kd);
}
EXPECT_GT(accepted, 0U);
}
TEST(Mitc4DrillingSelection, LegacyHeadFrozenEvidence) {
const auto nodes = Nodes();
const auto shell = LegacyShell(nodes);
const auto direction = ToVector(Direction());
const auto response =
fesa::EvaluateInternalResidualAndTangent(shell, Trial(), &direction);
ASSERT_TRUE(response.HasValue());
const auto& r = response.Value();
std::array<std::uint64_t, 8> hashes{};
hashes.fill(14695981039346656037ULL);
const auto add = [&](std::size_t channel, double value) {
std::uint64_t bits{};
std::memcpy(&bits, &value, sizeof(bits));
for (unsigned b = 0; b < 8; ++b) {
hashes[channel] ^= (bits >> (8 * b)) & 255ULL;
hashes[channel] *= 1099511628211ULL;
}
};
add(0, r.drilling_energy);
for (std::size_t i = 0; i < 24; ++i) {
add(1, r.drilling_force[i]);
add(4, r.force_directional_second[i]);
for (std::size_t j = 0; j < 24; ++j) {
add(2, r.drilling_tangent(i, j));
add(3, r.drilling_metric(i, j));
}
}
for (std::size_t n = 0; n < 4; ++n) {
add(5, r.drilling_constraint[n]);
for (std::size_t i = 0; i < 24; ++i) {
add(5, r.drilling_constraint_gradient(n, i));
add(7, r.drilling_constraint_third_directional_gradient[n][i]);
for (std::size_t j = 0; j < 24; ++j)
add(6, r.drilling_constraint_hessian[n](i, j));
}
}
// Captured before implementation from HEAD
// 52e321e15c70377d4318aace3f3bc64b3ea1a65c, native MSVC x64 Debug. Entire
// energy/force/tangent/metric/directional/psi inventories are pinned
// independently, including the historical polar1 constraint inconsistency. Do
// not regenerate these from the selected mode.
constexpr std::array<std::uint64_t, 8> frozen{
16395275265220000061ULL, 12405324468590059764ULL, 79034762627498281ULL,
7305417493946306304ULL, 7191018337857616990ULL, 4845391414006668707ULL,
11613163243225266145ULL, 3872161451216566265ULL};
EXPECT_EQ(hashes, frozen);
}
} // namespace