feat(hex8): use reference-weighted mean-volume energy

This commit is contained in:
KOKO\Mimi
2026-10-10 23:32:40 +09:00
parent 29670e168d
commit 18b36c1c22
8 changed files with 537 additions and 155 deletions
+227 -13
View File
@@ -11,6 +11,8 @@
#include <string>
#include <vector>
#include "../../../src/fesa/math/high_low_arithmetic.h"
namespace {
using Coordinates = fesa::Hex8Element::Coordinates;
constexpr int kSigns[8][3] = {{-1, -1, -1}, {1, -1, -1}, {1, 1, -1},
@@ -64,11 +66,14 @@ TEST(Hex8Element, ExactBrickStiffnessAndConsistentMass) {
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 = 48.0 * GradientProduct(a, b, i, j) +
double exact = -(2.0 / 3.0) * 48.0 * GradientProduct(a, b, i, j) +
48.0 * GradientProduct(a, b, j, i);
if (i == j)
for (std::size_t d = 0; d < 3; ++d)
exact += 48.0 * GradientProduct(a, b, d, d);
// Mean bulk term is the product of two independent first moments.
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);
}
@@ -542,8 +547,9 @@ TEST(Hex8Element, EnergyForceAndForceTangentDirectionalStepSweep) {
ASSERT_TRUE(trial.HasValue());
const double derivative = direction.Dot(trial.Value().internal_force);
const auto tangent_direction = trial.Value().tangent.Multiply(direction);
std::array<double, 6> energy_error{}, force_error{};
const std::array<double, 6> steps{1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7};
std::array<double, 8> energy_error{}, force_error{};
const std::array<double, 8> steps{1e-2, 1e-3, 1e-4, 1e-5,
1e-6, 1e-7, 1e-8, 1e-9};
for (std::size_t s = 0; s < steps.size(); ++s) {
auto plus = u, minus = u;
plus.Axpy(steps[s], direction);
@@ -751,14 +757,14 @@ TEST(Hex8Element, RejectsUnrepresentableShearAndMassInsteadOfReturningZeros) {
ASSERT_TRUE(element.HasValue());
EXPECT_FALSE(element.Value().ComputeMass().HasValue());
}
TEST(Hex8Element, RejectsRoundedLossOfPositiveVolumetricModulus) {
// Binary64 Lame arithmetic at this valid nu yields lambda+2mu/3 == 0
// even though exact E/[3(1-2nu)] is approximately1/9, which is positive.
TEST(Hex8Element, DirectBulkSurvivesNearNegativePoissonBound) {
const double nu = std::nextafter(-1.0, 0.0);
auto element = Make(Brick(), 1.0, nu);
ASSERT_FALSE(element.HasValue());
EXPECT_NE(element.GetStatus().Diagnostics()[0].message.find("bulk"),
std::string::npos);
ASSERT_TRUE(element.HasValue());
// Direct kappa is positive even where a lambda-based difference loses it.
EXPECT_GT(1.0 / (3.0 * (1.0 - 2.0 * nu)), 0.0);
fesa::Vector zero(24);
EXPECT_TRUE(element.Value().EvaluateLinear(zero).HasValue());
}
TEST(Hex8Element, OrdinaryNegativePoissonRatioRetainsPositiveVolumetricEnergy) {
auto element = Make(Brick(), 120.0, -.5);
@@ -838,9 +844,11 @@ Triple Cross(const Triple& a, const Triple& b) {
a[0] * b[1] - a[1] * b[0]};
}
std::array<double, 2> WarpedQuadratureEnergies(const fesa::Vector& u,
unsigned count) {
unsigned count,
bool nonlinear = false) {
const auto rule = LegendreRule(count);
std::array<double, 2> result{};
double volume = 0.0, trace_integral = 0.0;
for (const auto& xi : rule)
for (const auto& eta : rule)
for (const auto& zeta : rule) {
@@ -877,19 +885,30 @@ std::array<double, 2> WarpedQuadratureEnergies(const fesa::Vector& u,
for (std::size_t j = 0; j < 3; ++j)
for (std::size_t d = 0; d < 3; ++d)
gradient[i][j] += natural_gradient[i][d] * reciprocal[d][j];
Tensor strains{};
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) {
strains[i][j] = .5 * (gradient[i][j] + gradient[j][i]);
if (nonlinear)
for (std::size_t k = 0; k < 3; ++k)
strains[i][j] += .5 * gradient[k][i] * gradient[k][j];
}
double trace = 0.0, strain_squared = 0.0, velocity_squared = 0.0;
for (std::size_t i = 0; i < 3; ++i) {
trace += gradient[i][i];
trace += strains[i][i];
velocity_squared += velocity[i] * velocity[i];
for (std::size_t j = 0; j < 3; ++j) {
const double strain = .5 * (gradient[i][j] + gradient[j][i]);
const double strain = strains[i][j];
strain_squared += strain * strain;
}
}
const double weight = xi[1] * eta[1] * zeta[1] * det;
result[0] += weight * (24.0 * trace * trace + 48.0 * strain_squared);
result[0] += weight * 48.0 * (strain_squared - trace * trace / 3.0);
volume += weight;
trace_integral += weight * trace;
result[1] += weight * velocity_squared; // .5*rho*v^2 with rho=2.
}
result[0] += 40.0 * trace_integral * trace_integral / volume;
return result;
}
TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) {
@@ -922,4 +941,199 @@ TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) {
<< " K3-vs-K7=" << std::abs(q3[0] - q7[0]) / q7[0]
<< " K5-vs-K7=" << std::abs(q5[0] - q7[0]) / q7[0] << '\n';
}
Coordinates MeanVolumeUnitBrick() {
Coordinates x{};
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t i = 0; i < 3; ++i) x[a][i] = .5 * (kSigns[a][i] + 1);
return x;
}
TEST(Hex8Element,
MeanVolumeNonaffinePolynomialEnergyForceHessianAndActualStrain) {
const auto x = MeanVolumeUnitBrick();
const auto element = Make(x, 12.0, .25);
ASSERT_TRUE(element.HasValue());
constexpr double mu = 4.8, bulk = 8.0, c = .125;
// Exact monomial integrals of u_x=c*x*y over the unit cube. The other
// translations vanish; this direction varies trace among all eight points.
constexpr double a = 7 * mu / 18 + bulk / 8;
constexpr double b = 5 * mu / 18 + bulk / 6;
// Fixed two-point quadrature has Q[x^4]=7/36, not the continuum 1/5.
constexpr double d = 11 * mu / 108 + bulk / 18;
fesa::Vector u(24);
for (std::size_t n = 0; n < 8; ++n) u[3 * n] = c * x[n][0] * x[n][1];
for (bool nonlinear : {false, true}) {
const auto response = nonlinear ? element.Value().EvaluateNonlinear(u)
: element.Value().EvaluateLinear(u);
ASSERT_TRUE(response.HasValue());
const auto& r = response.Value();
const double energy =
a * c * c + (nonlinear ? b * c * c * c + d * c * c * c * c : 0);
const double force =
2 * a * c + (nonlinear ? 3 * b * c * c + 4 * d * c * c * c : 0);
const double hessian = 2 * a + (nonlinear ? 6 * b * c + 12 * d * c * c : 0);
EXPECT_NEAR(r.strain_energy, energy, 2e-14);
EXPECT_NEAR(r.internal_force[6] + r.internal_force[18], force, 2e-13);
EXPECT_NEAR(r.tangent(6, 6) + r.tangent(6, 18) + r.tangent(18, 6) +
r.tangent(18, 18),
hessian, 2e-12);
const double mean = c / 2 + (nonlinear ? c * c / 3 : 0);
for (const auto& point : r.points) {
const double px = point.reference_coordinates[0],
py = point.reference_coordinates[1];
const double t = 1 + c * py;
const double exx = c * py + (nonlinear ? .5 * c * c * py * py : 0);
const double eyy = nonlinear ? .5 * c * c * px * px : 0;
const double exy = .5 * c * px + (nonlinear ? .5 * c * c * px * py : 0);
const double trace = exx + eyy;
const double sxx = 2 * mu * (exx - trace / 3) + bulk * mean;
const double syy = 2 * mu * (eyy - trace / 3) + bulk * mean;
const double szz = -2 * mu * trace / 3 + bulk * mean;
const double sxy = 2 * mu * exy;
EXPECT_NEAR(point.strain[0], exx, 2e-15);
EXPECT_NEAR(point.strain[1], eyy, 2e-15);
EXPECT_NEAR(point.strain[3], exy, 2e-15);
EXPECT_NEAR(
point.cauchy_stress[0],
nonlinear
? (t * t * sxx + 2 * t * c * px * sxy + c * c * px * px * syy) / t
: sxx,
2e-13);
EXPECT_NEAR(point.cauchy_stress[1], nonlinear ? syy / t : syy, 2e-13);
EXPECT_NEAR(point.cauchy_stress[2], nonlinear ? szz / t : szz, 2e-13);
EXPECT_NEAR(point.cauchy_stress[3],
nonlinear ? (t * sxy + c * px * syy) / t : sxy, 2e-13);
}
}
const auto k = element.Value().ComputeStiffness().Value().values;
EXPECT_NEAR(k(0, 0), 41.0 / 18, 2e-14);
EXPECT_NEAR(k(0, 1), 19.0 / 30, 2e-14);
}
TEST(Hex8Element, MeanVolumeWarpedStressUsesReferenceWeightedTrace) {
auto element = Make(Warped());
ASSERT_TRUE(element.HasValue());
fesa::Vector u(24);
u[1] = 1; // Independent derivative coefficients separate the two means by
// >1e-8.
const auto response = element.Value().EvaluateLinear(u);
ASSERT_TRUE(response.HasValue());
const auto weights = element.Value().ReferenceWeights();
double volume = 0, integral = 0, arithmetic = 0;
for (std::size_t g = 0; g < 8; ++g) {
const auto& e = response.Value().points[g].strain;
const double trace = e[0] + e[1] + e[2];
volume += weights[g];
integral += weights[g] * trace;
arithmetic += trace / 8;
}
const double mean = integral / volume;
ASSERT_GT(std::abs(mean - arithmetic), 1e-8);
for (const auto& point : response.Value().points) {
const auto& s = point.cauchy_stress;
EXPECT_NEAR((s[0] + s[1] + s[2]) / 3, 80 * mean, 2e-13);
}
EXPECT_NEAR(response.Value().strain_energy, WarpedQuadratureEnergies(u, 2)[0],
2e-12);
}
TEST(Hex8Element,
MeanVolumeDirectBulkSupportsBoundsWithoutLambdaOrThreeBulkGuard) {
EXPECT_TRUE(
Make(MeanVolumeUnitBrick(), 12, std::nextafter(-1.0, 0.0)).HasValue());
EXPECT_TRUE(
Make(MeanVolumeUnitBrick(), 1, std::nextafter(.5, 0.0)).HasValue());
const auto huge = Make(MeanVolumeUnitBrick(), 1e308, .4);
ASSERT_TRUE(huge.HasValue());
const double direct_bulk = 1e308 / (3 * (1 - 2 * .4));
ASSERT_TRUE(std::isfinite(direct_bulk));
ASSERT_FALSE(std::isfinite(3 * direct_bulk));
fesa::Vector u(24);
constexpr double shear = 1e-150;
const auto x = MeanVolumeUnitBrick();
for (unsigned n = 0; n < 8; ++n) u[3 * n] = shear * x[n][1];
const auto response = huge.Value().EvaluateLinear(u);
ASSERT_TRUE(response.HasValue());
const double expected = .5 * (1e308 / (2 * (1 + .4))) * shear * shear;
EXPECT_NEAR(response.Value().strain_energy, expected, 1e-13 * expected);
EXPECT_FALSE(Make(MeanVolumeUnitBrick(), 1e308, .499).HasValue());
EXPECT_FALSE(Make(MeanVolumeUnitBrick(),
std::numeric_limits<double>::denorm_min(), -.5)
.HasValue());
}
TEST(Hex8Element,
MeanVolumeWarpedIndependentEnergyHasCoupledNodeZeroDerivatives) {
auto element = Make(Warped());
ASSERT_TRUE(element.HasValue());
fesa::Vector u(24), direction(24), zero(24);
for (std::size_t i = 0; i < 24; ++i) u[i] = .03 * std::sin(.7 * i);
direction[0] = 1;
direction[8] = -.375;
direction[22] = .25;
constexpr double h = .01;
for (bool nonlinear : {false, true}) {
const auto response = nonlinear ? element.Value().EvaluateNonlinear(u)
: element.Value().EvaluateLinear(u);
const auto paired = nonlinear
? element.Value().EvaluateNonlinearHighLow(u, zero)
: element.Value().EvaluateLinearHighLow(u, zero);
ASSERT_TRUE(response.HasValue());
ASSERT_TRUE(paired.HasValue());
const double energy = WarpedQuadratureEnergies(u, 2, nonlinear)[0];
EXPECT_NEAR(response.Value().strain_energy, energy, 2e-12);
EXPECT_NEAR(paired.Value().high.strain_energy, energy, 2e-12);
std::array<double, 2> first{}, second{};
for (unsigned level = 0; level < 2; ++level) {
const double step = h / (1U << level);
auto plus = u, minus = u;
plus.Axpy(step, direction);
minus.Axpy(-step, direction);
const double ep = WarpedQuadratureEnergies(plus, 2, nonlinear)[0];
const double em = WarpedQuadratureEnergies(minus, 2, nonlinear)[0];
first[level] = (ep - em) / (2 * step);
second[level] = (ep - 2 * energy + em) / (step * step);
}
// This energy is quartic in nodal translations; Richardson removes the
// exact centered-difference h² term, independent of either kernel path.
const double df = (4 * first[1] - first[0]) / 3;
const double ddf = (4 * second[1] - second[0]) / 3;
EXPECT_NEAR(direction.Dot(response.Value().internal_force), df, 2e-10);
EXPECT_NEAR(direction.Dot(paired.Value().high.internal_force), df, 2e-10);
EXPECT_NEAR(direction.Dot(response.Value().tangent.Multiply(direction)),
ddf, 2e-8);
EXPECT_NEAR(direction.Dot(paired.Value().high.tangent.Multiply(direction)),
ddf, 2e-8);
}
}
TEST(Hex8Element, MeanVolumeWarpedLowOnlyStateRetainsWeightedCubicEnergy) {
using namespace fesa::math_internal;
auto element = Make(Warped());
ASSERT_TRUE(element.HasValue());
const double c = std::ldexp(1.0, -60);
fesa::Vector high(24), low(24), zero(24), basis(24), minus(24);
for (unsigned a = 0; a < 8; ++a)
for (unsigned i = 0; i < 3; ++i) high[3 * a + i] = 1;
low[1] = c;
basis[1] = 1;
minus[1] = -1;
const auto linear = element.Value().EvaluateLinearHighLow(high, low);
const auto nonlinear = element.Value().EvaluateNonlinearHighLow(high, low);
const auto high_only = element.Value().EvaluateNonlinearHighLow(high, zero);
ASSERT_TRUE(linear.HasValue());
ASSERT_TRUE(nonlinear.HasValue());
ASSERT_TRUE(high_only.HasValue());
EXPECT_EQ(high_only.Value().high.strain_energy, 0);
const double quadratic = WarpedQuadratureEnergies(basis, 2)[0];
const double cubic = .5 * (WarpedQuadratureEnergies(basis, 2, true)[0] -
WarpedQuadratureEnergies(minus, 2, true)[0]);
EXPECT_NEAR(linear.Value().high.strain_energy, quadratic * c * c,
1e-12 * c * c);
const auto actual_cubic = Subtract(
{nonlinear.Value().high.strain_energy,
nonlinear.Value().strain_energy_low},
{linear.Value().high.strain_energy, linear.Value().strain_energy_low});
EXPECT_NEAR(actual_cubic.high, cubic * c * c * c, 1e-11 * c * c * c);
EXPECT_NE(actual_cubic.high, 0);
}
} // namespace