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
+10 -1
View File
@@ -395,6 +395,8 @@ TEST(ElementFactory, SolidBindingPreservesThreeComponentsAndReferenceMass) {
EXPECT_EQ(stiffness.Value().values.Rows(), 24U);
auto mass = solid->ComputeMass();
ASSERT_TRUE(mass.HasValue());
ASSERT_EQ(mass.Value().values.Rows(), 24U);
ASSERT_EQ(mass.Value().values.Columns(), 24U);
double total_mass = 0.0;
for (std::size_t a = 0U; a < 8U; ++a) {
for (std::size_t b = 0U; b < 8U; ++b) {
@@ -402,7 +404,14 @@ TEST(ElementFactory, SolidBindingPreservesThreeComponentsAndReferenceMass) {
}
}
EXPECT_NEAR(total_mass, 2.0, 1.0e-13);
EXPECT_NEAR(mass.Value().values(0U, 3U), 2.0 / 54.0, 1.0e-15);
for (std::size_t row = 0U; row < 24U; ++row) {
for (std::size_t column = 0U; column < 24U; ++column) {
if (row == column)
EXPECT_NEAR(mass.Value().values(row, column), 0.25, 1.0e-15);
else
EXPECT_DOUBLE_EQ(mass.Value().values(row, column), 0.0);
}
}
auto response = solid->EvaluateLinear(fesa::Vector{24U});
ASSERT_TRUE(response.HasValue());
for (std::size_t point = 0U; point < 8U; ++point) {
+64 -20
View File
@@ -48,7 +48,7 @@ double GradientProduct(std::size_t a, std::size_t b, std::size_t i,
if (d != i && d != j) integral *= 1.0 + kSigns[a][d] * kSigns[b][d] / 3.0;
return integral;
}
TEST(Hex8Element, ExactBrickStiffnessAndConsistentMass) {
TEST(Hex8Element, ExactBrickStiffnessAndRowSumLumpedMass) {
auto element = Make();
ASSERT_TRUE(element.HasValue());
auto stiffness = element.Value().ComputeStiffness();
@@ -61,9 +61,6 @@ TEST(Hex8Element, ExactBrickStiffnessAndConsistentMass) {
ASSERT_EQ(m.Columns(), 24U);
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t b = 0; b < 8; ++b) {
double scalar = 2.0 * 48.0 / 216.0;
for (std::size_t d = 0; d < 3; ++d)
scalar *= kSigns[a][d] == kSigns[b][d] ? 2.0 : 1.0;
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) {
double exact = -(2.0 / 3.0) * 48.0 * GradientProduct(a, b, i, j) +
@@ -75,7 +72,10 @@ TEST(Hex8Element, ExactBrickStiffnessAndConsistentMass) {
exact += (80.0 / 48.0) * (48.0 * kSigns[a][i] / (8.0 * (i + 1))) *
(48.0 * kSigns[b][j] / (8.0 * (j + 1)));
EXPECT_NEAR(k(3 * a + i, 3 * b + j), exact, 2e-12);
EXPECT_NEAR(m(3 * a + i, 3 * b + j), i == j ? scalar : 0.0, 2e-14);
// rho*V/8 = 2*48/8. This is the row sum of the complete
// consistent mass, before constraints can remove its columns.
const double expected_mass = a == b && i == j ? 12.0 : 0.0;
EXPECT_NEAR(m(3 * a + i, 3 * b + j), expected_mass, 2e-14);
}
}
}
@@ -351,24 +351,21 @@ TEST(Hex8Element, SixRigidModesAndZeroStateTangent) {
}
}
TEST(Hex8Element,
ConsistentMassHasAll24PositiveDirectionsAndReferenceInvariance) {
RowSumLumpedMassHasAll24PositiveDirectionsAndReferenceInvariance) {
auto element = Make();
ASSERT_TRUE(element.HasValue());
auto before = element.Value().ComputeMass().Value().values;
// Tensor-product Walsh vectors diagonalize the independent brick scalar
// mass; eigenvalues are rho*V/(8*3^number_of_odd_axes), each repeated3.
// The complete consistent mass is row-summed before constraints, giving
// rho*V/8 on every translational diagonal of this affine brick.
for (std::size_t mask = 0; mask < 8; ++mask)
for (std::size_t i = 0; i < 3; ++i) {
fesa::Vector v{24U};
unsigned odd = 0U;
for (std::size_t d = 0; d < 3; ++d)
if ((mask & (1U << d)) != 0U) ++odd;
for (std::size_t a = 0; a < 8; ++a) {
v[3 * a + i] = 1.0;
for (std::size_t d = 0; d < 3; ++d)
if ((mask & (1U << d)) != 0U) v[3 * a + i] *= kSigns[a][d];
}
const double eigenvalue = 12.0 / std::pow(3.0, odd);
const double eigenvalue = 12.0;
const auto mv = before.Multiply(v);
for (std::size_t a = 0; a < 24; ++a)
EXPECT_NEAR(mv[a], eigenvalue * v[a], 3e-14);
@@ -839,6 +836,27 @@ std::vector<std::array<double, 2>> LegendreRule(unsigned count) {
}
return points;
}
std::array<double, 8> WarpedLumpedMass3x3() {
const auto rule = LegendreRule(3);
std::array<double, 8> mass{};
for (const auto& xi : rule)
for (const auto& eta : rule)
for (const auto& zeta : rule) {
const std::array<double, 3> natural{xi[0], eta[0], zeta[0]};
// det(dX/d(xi,eta,zeta)) for Warped(), expanded independently from
// the production geometry and quadrature paths.
const double det =
6.0 - .0135 * xi[0] * xi[0] - .036 * eta[0] * eta[0] -
.0324 * zeta[0] * zeta[0] + .00324 * xi[0] * eta[0] * zeta[0];
for (std::size_t a = 0; a < 8; ++a) {
double shape = .125;
for (std::size_t d = 0; d < 3; ++d)
shape *= 1.0 + kSigns[a][d] * natural[d];
mass[a] += 2.0 * xi[1] * eta[1] * zeta[1] * shape * det;
}
}
return mass;
}
Triple Cross(const Triple& a, const Triple& b) {
return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]};
@@ -911,6 +929,40 @@ std::array<double, 2> WarpedQuadratureEnergies(const fesa::Vector& u,
result[0] += 40.0 * trace_integral * trace_integral / volume;
return result;
}
TEST(Hex8Element,
WarpedRowSumMassMatchesIndependentNodalIntegralsAndNonuniformEnergy) {
auto element = Make(Warped());
ASSERT_TRUE(element.HasValue());
const auto mass = element.Value().ComputeMass();
ASSERT_TRUE(mass.HasValue());
const auto expected = WarpedLumpedMass3x3();
const auto& actual = mass.Value().values;
double total = 0.0, min_nodal = expected[0], max_nodal = expected[0];
fesa::Vector velocity(24);
for (std::size_t a = 0; a < 8; ++a) {
total += expected[a];
min_nodal = (std::min)(min_nodal, expected[a]);
max_nodal = (std::max)(max_nodal, expected[a]);
velocity[3U * a] = static_cast<double>(a + 1U);
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j)
EXPECT_NEAR(actual(3U * a + i, 3U * a + j),
i == j ? expected[a] : 0.0, 3e-14);
}
// The xyz term in detJ gives two distinct nodal masses. A uniform rho*V/8
// shortcut preserves total mass but fails this kinetic-energy observable.
EXPECT_GT(max_nodal - min_nodal, 1e-4);
const double lumped_energy = .5 * velocity.Dot(actual.Multiply(velocity));
double independent_energy = 0.0, uniform_energy = 0.0;
for (std::size_t a = 0; a < 8; ++a) {
const double v2 = velocity[3U * a] * velocity[3U * a];
independent_energy += .5 * expected[a] * v2;
uniform_energy += .5 * total / 8.0 * v2;
}
EXPECT_NEAR(lumped_energy, independent_energy,
3e-14 * independent_energy);
EXPECT_GT(std::abs(lumped_energy - uniform_energy), 1e-4);
}
TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) {
auto element = Make(Warped());
ASSERT_TRUE(element.HasValue());
@@ -918,26 +970,18 @@ TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) {
for (std::size_t i = 0; i < 24; ++i)
u[i] = .03 * std::sin(.7 * static_cast<double>(i));
const auto k = element.Value().ComputeStiffness().Value().values;
const auto m = element.Value().ComputeMass().Value().values;
const auto q2 = WarpedQuadratureEnergies(u, 2),
q3 = WarpedQuadratureEnergies(u, 3);
const auto q5 = WarpedQuadratureEnergies(u, 5),
q7 = WarpedQuadratureEnergies(u, 7);
const double budget = 512.0 * std::numeric_limits<double>::epsilon();
EXPECT_NEAR(.5 * u.Dot(k.Multiply(u)), q2[0], budget * q2[0]);
EXPECT_NEAR(.5 * u.Dot(m.Multiply(u)), q2[1], budget * q2[1]);
const double stiffness_discrepancy = std::abs(q2[0] - q7[0]) / q7[0];
const double mass_discrepancy = std::abs(q2[1] - q7[1]) / q7[1];
// Mandated2x2 is retained: higher-order integration gives a distinct warped
// discretization. Rational stiffness integrands converge with refinement.
EXPECT_GT(stiffness_discrepancy, 1e-8);
EXPECT_GT(mass_discrepancy, 1e-8);
EXPECT_LT(std::abs(q5[0] - q7[0]), .05 * std::abs(q3[0] - q7[0]));
// N_a*N_b*detJ has per-axis degree<=4, so order3 integrates mass exactly.
EXPECT_NEAR(q3[1], q7[1], budget * q7[1]);
EXPECT_NEAR(q5[1], q7[1], budget * q7[1]);
std::cout << "warped K2-vs-K7 energy relative=" << stiffness_discrepancy
<< " M2-vs-M7 kinetic relative=" << mass_discrepancy
<< " K3-vs-K7=" << std::abs(q3[0] - q7[0]) / q7[0]
<< " K5-vs-K7=" << std::abs(q5[0] - q7[0]) / q7[0] << '\n';
}
@@ -79,18 +79,18 @@ fesa::Vector Difference(const fesa::Vector& left, const fesa::Vector& right,
return result;
}
TEST(NonlinearMitc4Inertia, IntegratesTheRectangleConsistentMassAndNull) {
TEST(NonlinearMitc4Inertia, IntegratesTheRectangleNodalLumpedMassAndNull) {
const auto nodes = Nodes();
const auto evaluation =
Inertia(nodes).Evaluate({}, fesa::Vector{24U}, fesa::Vector{24U});
ASSERT_TRUE(evaluation.HasValue());
const fesa::Matrix& mass = evaluation.Value().mass;
constexpr std::array<double, 4> kWeights{4.0, 2.0, 1.0, 2.0};
constexpr double kC0 = 0.8 / 36.0;
for (std::size_t row = 0U; row < 4U; ++row) {
for (std::size_t column = 0U; column < 4U; ++column) {
const std::size_t offset = (column + 4U - row) % 4U;
EXPECT_NEAR(mass(6U * row, 6U * column), kC0 * kWeights[offset], 1.0e-12);
EXPECT_NEAR(mass(6U * row, 6U * column), row == column ? 0.2 : 0.0,
1.0e-12);
EXPECT_NEAR(mass(6U * row + 3U, 6U * column + 3U),
row == column ? 0.2 * 0.2 * 0.2 / 12.0 : 0.0, 1.0e-12);
}
EXPECT_NEAR(mass(6U * row + 5U, 6U * row + 5U), 0.0, 1.0e-12);
}
@@ -122,6 +122,133 @@ TEST(NonlinearMitc4Inertia, UsesPhysicalAccelerationAndExactCurrentForce) {
}
}
TEST(NonlinearMitc4Inertia,
PreservesTinyAngularAccelerationAlongsideRigidTranslationAcceleration) {
for (const bool rotated : {false, true}) {
SCOPED_TRACE(rotated);
auto nodes = Nodes();
auto frames = Frames();
const auto rotation = fesa::ExpSkew(
rotated ? fesa::Vector3{0.4, -0.2, 0.7} : fesa::Vector3{});
const auto transform = [&](const std::array<double, 3>& value) {
std::array<double, 3> result{};
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j)
result[i] += rotation(i, j) * value[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 inertia = fesa::NonlinearMitc4Inertia::Create(
Pointers(nodes), frames, section, material, 1.0);
ASSERT_TRUE(inertia.HasValue());
fesa::Vector acceleration{24U};
for (std::size_t node = 0U; node < 4U; ++node)
for (std::size_t c = 0U; c < 3U; ++c) {
acceleration[6U * node + c] = c == 0U ? 15.0 : (c == 1U ? 12.0 : 0.0);
acceleration[6U * node + 3U + c] = 1e-17 * frames[node].tangent_a[c];
}
const auto actual = inertia.Value().Evaluate({}, fesa::Vector{24U}, acceleration);
ASSERT_TRUE(actual.HasValue());
// Nodal m=rho*A*t/4=.2, rotary inertia=m*t^2/12; c=0 at v=0.
constexpr double nodal_rotary_inertia = 0.2 * 0.2 * 0.2 / 12.0;
for (std::size_t node = 0U; node < 4U; ++node) {
double error = 0.0;
double scale = 0.0;
for (std::size_t c = 0U; c < 3U; ++c) {
const auto full = 6U * node + 3U + c;
const double expected = nodal_rotary_inertia * acceleration[full];
error = std::max(error, std::abs(actual.Value().force[full] - expected));
scale = std::max(scale, std::abs(expected));
EXPECT_DOUBLE_EQ(actual.Value().convective_force[full], 0.0);
}
ASSERT_GT(scale, 0.0);
EXPECT_LE(error / scale, 1e-12);
}
}
}
// At phi=0, Dq(M*a)[tangent_b]=-.5*j*epsilon*director for
// a_phi=epsilon*tangent_a. An origin at the active midpoint also isolates
// L=j*v_phi and Ldot=j*a_phi from orbital contributions.
TEST(NonlinearMitc4Inertia, MixedScaleRotationDerivativeAndAngularMomentum) {
for (const bool rotated : {false, true}) {
auto nodes = Nodes();
auto frames = Frames();
const auto rotation = fesa::ExpSkew(
rotated ? fesa::Vector3{0.4, -0.2, 0.7} : fesa::Vector3{});
const auto transform = [&](const std::array<double, 3>& value) {
std::array<double, 3> result{};
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j)
result[i] += rotation(i, j) * value[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 auto origin = nodes[0].coordinates;
for (auto& node : nodes)
for (std::size_t c = 0U; c < 3U; ++c)
node.coordinates[c] -= origin[c];
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 inertia = fesa::NonlinearMitc4Inertia::Create(
Pointers(nodes), frames, section, material, 1.0);
ASSERT_TRUE(inertia.HasValue());
constexpr double nodal_rotary_inertia = 0.2 * 0.2 * 0.2 / 12.0;
for (int power = 7; power <= 20; ++power) {
SCOPED_TRACE(rotated);
SCOPED_TRACE(power);
const double epsilon = std::pow(10.0, -power);
fesa::Vector acceleration{24U};
acceleration[0] = 15.0;
acceleration[1] = 12.0;
for (std::size_t c = 0U; c < 3U; ++c)
acceleration[3U + c] = epsilon * frames[0].tangent_a[c];
const auto actual = inertia.Value().Evaluate(
{}, fesa::Vector{24U}, acceleration);
ASSERT_TRUE(actual.HasValue());
double derivative_error = 0.0;
double rate_error = 0.0;
for (std::size_t c = 0U; c < 3U; ++c) {
double derivative = 0.0;
for (std::size_t k = 0U; k < 3U; ++k)
derivative += actual.Value().position_tangent(3U + c, 3U + k) *
frames[0].tangent_b[k];
derivative_error = std::max(derivative_error,
std::abs(derivative + 0.5 * nodal_rotary_inertia * epsilon *
frames[0].director[c]));
rate_error = std::max(rate_error,
std::abs(actual.Value().angular_momentum_rate[c] -
nodal_rotary_inertia * acceleration[3U + c]));
}
EXPECT_LE(derivative_error / (nodal_rotary_inertia * epsilon), 1e-12);
EXPECT_LE(rate_error / (nodal_rotary_inertia * epsilon), 1e-12);
const auto moving = inertia.Value().Evaluate(
{}, acceleration, fesa::Vector{24U});
ASSERT_TRUE(moving.HasValue());
double momentum_error = 0.0;
for (std::size_t c = 0U; c < 3U; ++c)
momentum_error = std::max(momentum_error,
std::abs(moving.Value().angular_momentum[c] -
nodal_rotary_inertia * acceleration[3U + c]));
EXPECT_LE(momentum_error / (nodal_rotary_inertia * epsilon), 1e-12);
}
}
}
using V3 = fesa::Vector3;
V3 Multiply(const fesa::Matrix& matrix, const V3& value) {
@@ -240,6 +367,48 @@ Fixture Warped() {
return fixture;
}
Fixture WarpedNonuniformDirectors() {
Fixture fixture = Warped();
for (const std::size_t node : {0U, 3U, 1U, 2U}) {
const double angle = (node == 0U || node == 3U) ? -0.1 : 0.1;
const double cosine = std::cos(angle);
const double sine = std::sin(angle);
fixture.frames[node].tangent_a = {cosine, 0.0, -sine};
fixture.frames[node].tangent_b = {0.0, 1.0, 0.0};
fixture.frames[node].director = {sine, 0.0, cosine};
}
return fixture;
}
V3 RotateFrame(const V3& value) {
constexpr double kCosZ = 0.7648421872844885;
constexpr double kSinZ = 0.6442176872376910;
constexpr double kCosX = 0.9210609940028851;
constexpr double kSinX = 0.3894183423086505;
return {kCosZ * value.X() - kSinZ * kCosX * value.Y() +
kSinZ * kSinX * value.Z(),
kSinZ * value.X() + kCosZ * kCosX * value.Y() -
kCosZ * kSinX * value.Z(),
kSinX * value.Y() + kCosX * value.Z()};
}
Fixture FlatRotatedLargeOffset() {
Fixture fixture;
for (std::size_t node = 0U; node < 4U; ++node) {
fixture.nodes[node].coordinates =
(RotateFrame(V3{fixture.nodes[node].coordinates}) +
V3{1.0e8, -2.0e8, 3.0e8})
.Components();
fixture.frames[node].director =
RotateFrame(V3{fixture.frames[node].director}).Components();
fixture.frames[node].tangent_a =
RotateFrame(V3{fixture.frames[node].tangent_a}).Components();
fixture.frames[node].tangent_b =
RotateFrame(V3{fixture.frames[node].tangent_b}).Components();
}
return fixture;
}
struct State {
fesa::NonlinearMitc4TrialDofs q{};
fesa::Vector v{24U};
@@ -272,6 +441,44 @@ fesa::Vector ApplyD(fesa::Vector vector, const double length) {
return vector;
}
TEST(NonlinearMitc4Inertia,
FlatRotatedLargeOffsetKeepsCenteredRotationalInertiaExactlyZero) {
const Fixture fixture = FlatRotatedLargeOffset();
const auto inertia = fixture.Create();
ASSERT_TRUE(inertia.HasValue());
fesa::Vector acceleration{24U};
for (std::size_t node = 0U; node < 4U; ++node) {
acceleration[6U * node] = 0.3;
acceleration[6U * node + 1U] = -0.2;
acceleration[6U * node + 2U] = 0.1;
}
const auto value = inertia.Value().Evaluate({}, fesa::Vector{24U}, acceleration);
ASSERT_TRUE(value.HasValue());
const auto mass_times_acceleration = value.Value().mass.Multiply(acceleration);
for (std::size_t node = 0U; node < 4U; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
EXPECT_DOUBLE_EQ(value.Value().force[6U * node + 3U + component], 0.0);
EXPECT_DOUBLE_EQ(mass_times_acceleration[6U * node + 3U + component],
0.0);
}
}
}
TEST(NonlinearMitc4Inertia, WarpedGeometryRetainsNonzeroFirstThicknessMoment) {
const auto inertia = WarpedNonuniformDirectors().Create();
ASSERT_TRUE(inertia.HasValue());
const auto value = inertia.Value().Evaluate({}, fesa::Vector{24U}, fesa::Vector{24U});
ASSERT_TRUE(value.HasValue());
double coupling = 0.0;
for (std::size_t row = 0U; row < 24U; ++row) {
for (std::size_t column = 0U; column < 24U; ++column) {
if (row % 6U >= 3U && column % 6U < 3U)
coupling = std::hypot(coupling, value.Value().mass(row, column));
}
}
EXPECT_GT(coupling, 0.0);
}
fesa::Matrix ScaledMatrix(const fesa::Matrix& matrix, const double length) {
fesa::Matrix result{matrix.Rows(), matrix.Columns()};
for (std::size_t i = 0; i < matrix.Rows(); ++i) {
@@ -459,7 +666,8 @@ struct Integral {
V3 angular_momentum_rate{};
};
Integral IntegrateFivePoints(const Fixture& fixture, const State& state) {
Integral IntegrateFivePoints(const Fixture& fixture, const State& state,
const bool lumped = true) {
const double r = std::sqrt(10.0 / 7.0);
const double outer = std::sqrt(5.0 + 2.0 * r) / 3.0;
const double inner = std::sqrt(5.0 - 2.0 * r) / 3.0;
@@ -490,63 +698,76 @@ Integral IntegrateFivePoints(const Fixture& fixture, const State& state) {
for (std::size_t xi = 0; xi < 5; ++xi) {
for (std::size_t eta = 0; eta < 5; ++eta) {
for (std::size_t zeta = 0; zeta < 5; ++zeta) {
const double z = 0.5 * fixture.thickness * points[zeta];
std::array<double, 4> shape{};
std::array<V3, 24> h{};
V3 a1{};
V3 a2{};
V3 a3{};
V3 position{};
V3 velocity{};
V3 acceleration{};
for (std::size_t node = 0; node < 4; ++node) {
shape[node] = 0.25 * (1.0 + sx[node] * points[xi]) *
(1.0 + sy[node] * points[eta]);
const double dx = 0.25 * sx[node] * (1.0 + sy[node] * points[eta]);
const double dy = 0.25 * sy[node] * (1.0 + sx[node] * points[xi]);
const V3 d0{fixture.frames[node].director};
const V3 x0{fixture.nodes[node].coordinates};
a1 = a1 + dx * (x0 + z * d0);
a2 = a2 + dy * (x0 + z * d0);
a3 = a3 + 0.5 * fixture.thickness * shape[node] * d0;
position = position + shape[node] * (x0 + state.q.displacement[node] +
z * directors[node]);
acceleration = acceleration + z * shape[node] * convective[node];
for (std::size_t j = 0; j < 3; ++j) {
std::array<double, 3> axis{};
axis[j] = shape[node];
h[6U * node + j] = V3{axis};
h[6U * node + 3U + j] = z * shape[node] * first[node][j];
for (std::size_t mass_node = 0; mass_node < (lumped ? 4U : 1U);
++mass_node) {
std::array<double, 4> kinetic_shape{};
kinetic_shape[mass_node] = 1.0;
const double z = 0.5 * fixture.thickness * points[zeta];
std::array<double, 4> shape{};
std::array<V3, 24> h{};
V3 a1{};
V3 a2{};
V3 a3{};
V3 position{};
V3 velocity{};
V3 acceleration{};
for (std::size_t node = 0; node < 4; ++node) {
shape[node] = 0.25 * (1.0 + sx[node] * points[xi]) *
(1.0 + sy[node] * points[eta]);
if (!lumped) kinetic_shape[node] = shape[node];
const double dx = 0.25 * sx[node] * (1.0 + sy[node] * points[eta]);
const double dy = 0.25 * sy[node] * (1.0 + sx[node] * points[xi]);
const V3 d0{fixture.frames[node].director};
const V3 x0{fixture.nodes[node].coordinates};
a1 = a1 + dx * (x0 + z * d0);
a2 = a2 + dy * (x0 + z * d0);
a3 = a3 + 0.5 * fixture.thickness * shape[node] * d0;
position = position +
kinetic_shape[node] * (x0 + state.q.displacement[node] +
z * directors[node]);
acceleration =
acceleration + z * kinetic_shape[node] * convective[node];
for (std::size_t j = 0; j < 3; ++j) {
std::array<double, 3> axis{};
axis[j] = kinetic_shape[node];
h[6U * node + j] = V3{axis};
h[6U * node + 3U + j] = z * kinetic_shape[node] * first[node][j];
}
}
}
const double weight = fixture.density * weights[xi] * weights[eta] *
weights[zeta] * a1.Dot(a2.Cross(a3));
EXPECT_GT(weight, 0.0);
integral.total_mass += weight;
for (std::size_t i = 0; i < 24; ++i) {
velocity = velocity + h[i] * state.v[i];
acceleration = acceleration + h[i] * state.a[i];
}
integral.energy += 0.5 * weight * velocity.Dot(velocity);
integral.power += weight * velocity.Dot(acceleration);
integral.momentum = integral.momentum + weight * velocity;
integral.angular_momentum =
integral.angular_momentum + weight * position.Cross(velocity);
integral.momentum_rate = integral.momentum_rate + weight * acceleration;
integral.angular_momentum_rate = integral.angular_momentum_rate +
weight * position.Cross(acceleration);
for (std::size_t i = 0; i < 24; ++i) {
integral.force[i] += weight * h[i].Dot(acceleration);
for (std::size_t j = 0; j < 24; ++j) {
integral.mass(i, j) += weight * h[i].Dot(h[j]);
const double weight = fixture.density * weights[xi] * weights[eta] *
weights[zeta] * a1.Dot(a2.Cross(a3)) *
(lumped ? shape[mass_node] : 1.0);
EXPECT_GT(weight, 0.0);
integral.total_mass += weight;
for (std::size_t i = 0; i < 24; ++i) {
velocity = velocity + h[i] * state.v[i];
acceleration = acceleration + h[i] * state.a[i];
}
}
for (std::size_t i = 0; i < 4; ++i) {
for (std::size_t j = 0; j < 4; ++j) {
double moment = weight * shape[i] * shape[j];
for (std::size_t k = 0; k < 3; ++k) {
integral.moments[k][i][j] += moment;
moment *= z;
integral.energy += 0.5 * weight * velocity.Dot(velocity);
integral.power += weight * velocity.Dot(acceleration);
integral.momentum = integral.momentum + weight * velocity;
integral.angular_momentum =
integral.angular_momentum + weight * position.Cross(velocity);
integral.momentum_rate =
integral.momentum_rate + weight * acceleration;
integral.angular_momentum_rate =
integral.angular_momentum_rate +
weight * position.Cross(acceleration);
for (std::size_t i = 0; i < 24; ++i) {
integral.force[i] += weight * h[i].Dot(acceleration);
for (std::size_t j = 0; j < 24; ++j) {
integral.mass(i, j) += weight * h[i].Dot(h[j]);
}
}
for (std::size_t i = 0; i < 4; ++i) {
for (std::size_t j = 0; j < 4; ++j) {
double moment =
lumped ? (i == mass_node && j == mass_node ? weight : 0.0)
: weight * shape[i] * shape[j];
for (std::size_t k = 0; k < 3; ++k) {
integral.moments[k][i][j] += moment;
moment *= z;
}
}
}
}
@@ -557,8 +778,9 @@ Integral IntegrateFivePoints(const Fixture& fixture, const State& state) {
}
TEST(NonlinearMitc4Inertia,
MatchesIndependentWarpedVolumeMomentsEnergyAndPower) {
for (const Fixture fixture : {Fixture{}, Warped()}) {
MatchesIndependentWarpedNodalMomentsEnergyAndPower) {
for (const Fixture fixture :
{Fixture{}, Warped(), WarpedNonuniformDirectors()}) {
const auto inertia = fixture.Create();
ASSERT_TRUE(inertia.HasValue());
const State state = FiniteState();
@@ -658,6 +880,39 @@ TEST(NonlinearMitc4Inertia,
}
}
TEST(NonlinearMitc4Inertia,
InitialMassMatchesEigenLumpingAndFiniteEnergyObeysJensen) {
for (const Fixture fixture : {Fixture{}, Warped()}) {
std::array<std::array<double, 3>, 4> directors{};
for (std::size_t node = 0; node < 4; ++node)
directors[node] = fixture.frames[node].director;
const auto shell = fesa::Mitc4Shell::Create(
Pointers(fixture.nodes), directors,
{"Section", fixture.thickness, 0U, {"mass-test", 1U}},
{"Material", fixture.modulus, 0.25, {"mass-test", 2U}});
ASSERT_TRUE(shell.HasValue());
const auto eigen = shell.Value().Mass(fixture.density);
ASSERT_TRUE(eigen.HasValue());
const auto inertia = fixture.Create();
ASSERT_TRUE(inertia.HasValue());
const auto initial =
inertia.Value().Evaluate({}, fesa::Vector{24U}, fesa::Vector{24U});
ASSERT_TRUE(initial.HasValue());
for (std::size_t i = 0; i < 24; ++i) {
for (std::size_t j = 0; j < 24; ++j) {
EXPECT_NEAR(initial.Value().mass(i, j),
eigen.Value().lumped_global24(i, j), 1.0e-12);
if (i / 6U != j / 6U) EXPECT_EQ(initial.Value().mass(i, j), 0.0);
}
}
const State state = FiniteState();
const auto lumped = IntegrateFivePoints(fixture, state, true);
const auto consistent = IntegrateFivePoints(fixture, state, false);
EXPECT_GT(lumped.energy - consistent.energy, 0.0);
EXPECT_NEAR(lumped.total_mass, consistent.total_mass, 1.0e-12);
}
}
TEST(DynamicRotation, ThirdDerivativesMatchAllPermutationsAndInteriorSweep) {
const std::array<V3, 5> states{
V3{}, V3{1.0e-6, -2.0e-6, 3.0e-6}, V3{0.2, -0.3, 0.4},
@@ -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