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
@@ -24,7 +24,7 @@ AffineMotion Motion(double time, bool nonlinear) {
const double dq = 2 * std::sin(2 * time);
const double ddq = 4 * std::cos(2 * time);
AffineMotion m;
constexpr double lambda = 4.8;
constexpr double bulk = 8;
constexpr double mu = 4.8;
if (!nonlinear) {
const M3 d{{{.015, .008, -.004}, {.006, -.01, .003}, {.005, .004, .012}}};
@@ -35,8 +35,8 @@ AffineMotion Motion(double time, bool nonlinear) {
m.b[i][j] = q * d[i][j];
m.velocity[i][j] = dq * d[i][j];
m.acceleration[i][j] = ddq * d[i][j];
m.piola[i][j] =
(i == j ? lambda * trace : 0) + mu * q * (d[i][j] + d[j][i]);
m.piola[i][j] = (i == j ? (bulk * trace - 2 * mu * trace / 3) : 0) +
mu * q * (d[i][j] + d[j][i]);
}
} else {
const double s = .004 * q, ds = .004 * dq, dds = .004 * ddq;
@@ -46,7 +46,7 @@ AffineMotion Motion(double time, bool nonlinear) {
const M3 jr{{{-sn, -c, 0}, {c, -sn, 0}, {0, 0, 0}}};
const M3 jjr{{{-c, sn, 0}, {-sn, -c, 0}, {0, 0, 0}}};
const double eg = .5 * ((1 + s) * (1 + s) - 1);
const double stress = (3 * lambda + 2 * mu) * eg;
const double stress = 3 * bulk * eg;
for (unsigned i = 0; i < 3; ++i)
for (unsigned j = 0; j < 3; ++j) {
m.b[i][j] = (1 + s) * r[i][j] - (i == j ? 1 : 0);
@@ -506,7 +506,7 @@ TEST(SolidHhtTemporal,
TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
// Node0's three translations are free. Exact polynomial integrals yield
// M=(rho/27)I, Kii=(lambda+4mu)/9, Kij=(lambda+mu)/12.
// M=(rho/27)I, Kii=41/18, Kij=19/30 for mu=4.8 and direct kappa=8.
std::array<std::array<double, 3>, 3> errors{};
for (unsigned level = 0; level < 3; ++level) {
const double h = .02 / static_cast<double>(1U << level);
@@ -529,8 +529,8 @@ TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
<< solid_hht_test::Message(result.GetStatus());
const auto& f = result.Value().state.CommittedState();
const double m = 2.0 / 27;
const double diagonal = (4.8 + 4 * 4.8) / 9;
const double off = (4.8 + 4.8) / 12;
const double diagonal = 41.0 / 18;
const double off = 19.0 / 30;
const std::array<double, 2> k{diagonal + 2 * off, diagonal - off};
for (unsigned c = 0; c < 3; ++c) {
std::array<double, 3> truth{};
@@ -563,7 +563,7 @@ TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
TEST(SolidHhtTemporal, IndependentScalarBrickOscillatorHasSecondOrderUVA) {
std::array<std::array<double, 3>, 3> errors{};
const double m = 2.0 / 27;
const double k = (4.8 + 4 * 4.8) / 9;
const double k = 41.0 / 18;
const double w = std::sqrt(k / m);
for (unsigned level = 0; level < 3; ++level) {
const double h = .02 / static_cast<double>(1U << level);
@@ -608,46 +608,95 @@ TEST(SolidHhtTemporal, IndependentScalarBrickOscillatorHasSecondOrderUVA) {
}
}
TEST(SolidHhtTemporal,
BendingMeshRefinementQuantifiesFullIntegrationShearLocking) {
constexpr double thickness = .125;
constexpr double curvature = .01;
constexpr double lambda = 4.8;
constexpr double mu = 4.8;
const double continuum = .5 * (lambda + 2 * mu) * curvature * curvature *
thickness * thickness * thickness / 12;
double previous_error = 0;
for (unsigned nx : {1U, 2U, 4U, 8U}) {
double numerical = 0;
for (unsigned e = 0; e < nx; ++e) {
auto d = solid_hht_test::Brick();
namespace {
// Compatible nodal pure-bending field; no equilibrium solve is claimed.
std::array<double, 3> BendingEnergy(unsigned nx, unsigned ny) {
constexpr double thickness = .125, curvature = .01, bulk = 8;
std::array<double, 3> result{};
auto d = solid_hht_test::Brick();
for (unsigned ex = 0; ex < nx; ++ex)
for (unsigned ey = 0; ey < ny; ++ey) {
const auto index = ex * ny + ey;
fesa::Hex8Element::Coordinates x{};
fesa::ElementDofLayout layout{
{"I", e + 1, std::to_string(e + 1)},
{"I", index + 1, std::to_string(index + 1)},
{0, 1, 2, 3, 4, 5, 6, 7},
{fesa::DofComponent::kUx, fesa::DofComponent::kUy,
fesa::DofComponent::kUz}};
fesa::Vector u(24);
for (unsigned a = 0; a < 8; ++a) {
x[a] = {static_cast<double>(e) / nx + d.nodes[a].coordinates[0] / nx,
thickness * (d.nodes[a].coordinates[1] - .5),
x[a] = {(ex + d.nodes[a].coordinates[0]) / nx,
thickness * ((ey + d.nodes[a].coordinates[1]) / ny - .5),
d.nodes[a].coordinates[2]};
u[3 * a] = -curvature * x[a][0] * x[a][1];
u[3 * a + 1] = .5 * curvature * x[a][0] * x[a][0];
}
auto element = fesa::Hex8Element::Create(e, layout, x, d.materials[0], 2);
ASSERT_TRUE(element.HasValue());
auto element =
fesa::Hex8Element::Create(index, layout, x, d.materials[0], 2);
EXPECT_TRUE(element.HasValue());
if (!element.HasValue()) return {};
auto response = element.Value().EvaluateLinear(u);
ASSERT_TRUE(response.HasValue());
numerical += response.Value().strain_energy;
EXPECT_TRUE(response.HasValue());
if (!response.HasValue()) return {};
const auto weights = element.Value().ReferenceWeights();
double volume = 0, trace_integral = 0;
for (unsigned g = 0; g < 8; ++g) {
volume += weights[g];
const auto& strain = response.Value().points[g].strain;
trace_integral += weights[g] * (strain[0] + strain[1] + strain[2]);
}
const double volumetric =
.5 * bulk * trace_integral * trace_integral / volume;
result[0] += response.Value().strain_energy;
result[1] += response.Value().strain_energy - volumetric;
result[2] += volumetric;
}
const double error = numerical / continuum - 1;
const double exact_parasitic =
mu / (lambda + 2 * mu) / std::pow(nx * thickness, 2);
EXPECT_NEAR(error, exact_parasitic, 2e-11);
if (previous_error != 0) EXPECT_NEAR(previous_error / error, 4, 1e-10);
std::cout << "Bending nx=" << nx
<< " energy/continuum=" << numerical / continuum << '\n';
previous_error = error;
return result;
}
} // namespace
TEST(SolidHhtTemporal, FixedThicknessBendingRetainsDiscreteBulkBias) {
constexpr double thickness = .125, curvature = .01, mu = 4.8, bulk = 8;
constexpr double axial = bulk + 4 * mu / 3;
const double continuum =
axial * curvature * curvature * std::pow(thickness, 3) / 24;
const double limit = 4 * mu / (3 * axial);
double previous_parasitic = 0;
for (unsigned nx : {1U, 2U, 4U, 8U}) {
const auto energy = BendingEnergy(nx, 1);
const double parasitic = mu / (axial * std::pow(nx * thickness, 2));
EXPECT_NEAR(energy[0] / continuum, limit + parasitic, 2e-11);
EXPECT_NEAR(energy[2] / continuum, 0, 2e-13);
if (previous_parasitic != 0)
EXPECT_NEAR(previous_parasitic / parasitic, 4, 1e-13);
previous_parasitic = parasitic;
std::cout << "Fixed thickness nx=" << nx
<< " U/Ucont=" << energy[0] / continuum
<< " limiting ratio=" << limit << " shear=" << parasitic << '\n';
}
}
TEST(SolidHhtTemporal,
BendingMeshRefinementQuantifiesFullIntegrationShearLocking) {
constexpr double thickness = .125, curvature = .01, mu = 4.8, bulk = 8;
constexpr double axial = bulk + 4 * mu / 3;
const double continuum =
axial * curvature * curvature * std::pow(thickness, 3) / 24;
double previous_error = 0;
for (unsigned n : {1U, 2U, 4U, 8U}) {
const auto energy = BendingEnergy(n, n);
const double shear = mu / (axial * std::pow(n * thickness, 2));
const double missing_bulk = bulk / (axial * n * n);
const double error = energy[0] / continuum - 1;
EXPECT_NEAR(error, shear - missing_bulk, 2e-11);
// Independent moments separate competing errors, so cancellation cannot
// make the continuum-convergence claim pass accidentally.
EXPECT_NEAR(energy[1] / continuum, 4 * mu / (3 * axial) + shear, 2e-11);
EXPECT_NEAR(energy[2] / continuum, bulk / axial - missing_bulk, 2e-11);
if (previous_error != 0) EXPECT_NEAR(previous_error / error, 4, 1e-10);
previous_error = error;
std::cout << "Bending nx=ny=" << n << " U/Ucont=" << energy[0] / continuum
<< " positive shear=" << shear
<< " negative bulk=" << missing_bulk << '\n';
}
}