feat(hex8): integrate paired HHT dynamic state and bounded verification

This commit is contained in:
KOKO\Mimi
2026-10-10 00:36:16 +09:00
parent c7c3c8017f
commit 338902974c
17 changed files with 4291 additions and 11 deletions
+2
View File
@@ -19,6 +19,8 @@ add_executable(
unit/analysis/nonlinear_static_control_test.cpp
unit/analysis/newton_iteration_log_test.cpp
unit/analysis/solid_frequency_procedure_test.cpp
unit/analysis/solid_hht_dynamic_procedure_test.cpp
unit/analysis/solid_hht_temporal_verification_test.cpp
unit/analysis/solid_static_procedure_test.cpp
unit/analysis/solid_static_state_test.cpp
unit/analysis/thermal_time_grid_test.cpp
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,653 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <limits>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/solid_hht_dynamic_procedure.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/hex8_element.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
#include "solid_hht_test_fixture.h"
namespace {
using M3 = std::array<std::array<double, 3>, 3>;
struct AffineMotion {
M3 b{}, velocity{}, acceleration{}, piola{};
};
AffineMotion Motion(double time, bool nonlinear) {
const double q = 1 - std::cos(2 * time);
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 mu = 4.8;
if (!nonlinear) {
const M3 d{{{.015, .008, -.004}, {.006, -.01, .003}, {.005, .004, .012}}};
double trace = 0;
for (unsigned i = 0; i < 3; ++i) trace += q * d[i][i];
for (unsigned i = 0; i < 3; ++i)
for (unsigned j = 0; j < 3; ++j) {
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]);
}
} else {
const double s = .004 * q, ds = .004 * dq, dds = .004 * ddq;
const double theta = .5 * q, dt = .5 * dq, ddt = .5 * ddq;
const double c = std::cos(theta), sn = std::sin(theta);
const M3 r{{{c, -sn, 0}, {sn, c, 0}, {0, 0, 1}}};
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;
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);
m.velocity[i][j] = ds * r[i][j] + (1 + s) * dt * jr[i][j];
m.acceleration[i][j] = dds * r[i][j] + 2 * ds * dt * jr[i][j] +
(1 + s) * (ddt * jr[i][j] + dt * dt * jjr[i][j]);
m.piola[i][j] = (1 + s) * r[i][j] * stress;
}
}
return m;
}
std::array<double, 3> Apply(const M3& matrix, const std::array<double, 3>& x) {
std::array<double, 3> result{};
for (unsigned i = 0; i < 3; ++i)
for (unsigned j = 0; j < 3; ++j) result[i] += matrix[i][j] * x[j];
return result;
}
fesa::ModelDefinition TwoBricks(double h, unsigned count, bool nonlinear) {
auto d = solid_hht_test::Brick(h, count, nonlinear);
const std::array<std::array<double, 3>, 4> added{
{{2, 0, 0}, {2, 1, 0}, {2, 0, 1}, {2, 1, 1}}};
for (unsigned j = 0; j < 4; ++j) {
unsigned n = 8 + j;
d.nodes.push_back({{"I", n + 1, std::to_string(n + 1)}, added[j], {}});
d.node_sets.push_back({"N" + std::to_string(n), {}, {n}, {}});
}
d.solid_elements.emplace_back(
fesa::SourceEntityId{"I", 9, "9"},
std::array<fesa::EntityIndex, 8>{1, 8, 9, 2, 5, 10, 11, 6}, 0, 0,
fesa::SourceLocation{});
return d;
}
// Closed polynomial brick integrals: integral Na*X and integral grad Na.
// These do not call the production stiffness, mass, quadrature or response.
std::vector<double> ExactForce(const fesa::ModelDefinition& d, double t,
bool nonlinear) {
const auto m = Motion(t, nonlinear);
std::vector<double> f(3 * d.nodes.size());
for (unsigned e = 0; e < 2; ++e) {
const auto& nodes = d.solid_elements[e].node_indices;
for (unsigned a = 0; a < 8; ++a) {
const auto& x = d.nodes[nodes[a]].coordinates;
std::array<double, 3> integral_x{}, gradient{};
for (unsigned c = 0; c < 3; ++c) {
const double origin = c == 0 ? static_cast<double>(e) : 0;
const double local = x[c] - origin;
integral_x[c] = 2 * (origin / 8 + (1 + local) / 24);
gradient[c] = (2 * local - 1) / 4;
}
const auto inertia = Apply(m.acceleration, integral_x);
const auto internal = Apply(m.piola, gradient);
for (unsigned c = 0; c < 3; ++c)
f[3 * nodes[a] + c] += inertia[c] + internal[c];
}
}
return f;
}
std::vector<double> ExactMassProduct(const fesa::Domain& d,
const fesa::Vector& v) {
std::vector<double> result(v.Size());
for (unsigned e = 0; e < 2; ++e) {
const auto& nodes = d.SolidElements()[e].node_indices;
for (unsigned a = 0; a < 8; ++a)
for (unsigned b = 0; b < 8; ++b) {
double coefficient = 2.0 / 216;
for (unsigned c = 0; c < 3; ++c)
coefficient *= d.Nodes()[nodes[a]].coordinates[c] ==
d.Nodes()[nodes[b]].coordinates[c]
? 2
: 1;
for (unsigned c = 0; c < 3; ++c)
result[3 * nodes[a] + c] += coefficient * v[3 * nodes[b] + c];
}
}
return result;
}
void AddCross(const std::array<double, 3>& x, const std::array<double, 3>& f,
std::array<double, 3>* result) {
(*result)[0] += x[1] * f[2] - x[2] * f[1];
(*result)[1] += x[2] * f[0] - x[0] * f[2];
(*result)[2] += x[0] * f[1] - x[1] * f[0];
}
std::array<double, 3> ManufacturedRun(double h, unsigned count,
bool nonlinear,
bool compare_parallel = false) {
auto d = TwoBricks(h, count, nonlinear);
for (unsigned i = 0; i < 3 * d.nodes.size(); ++i) {
fesa::TabularAmplitudeDefinition amplitude;
amplitude.name_raw = "A" + std::to_string(i);
amplitude.name_canonical = amplitude.name_raw;
for (unsigned n = 0; n <= count; ++n) {
const double t = static_cast<double>(n) * h;
amplitude.points.push_back({t, ExactForce(d, t, nonlinear)[i]});
}
d.amplitudes.push_back(amplitude);
d.dynamic_step->loads.push_back({"N" + std::to_string(i / 3),
static_cast<int>(i % 3 + 1),
1,
{},
i,
amplitude.name_raw,
{}});
}
auto domain = fesa::Domain::Create(std::move(d));
EXPECT_TRUE(domain.HasValue());
if (!domain.HasValue()) return {1, 1, 1};
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
auto model = fesa::AnalysisModel::Create(domain.Value());
auto dofs = fesa::DofManager::Create(model.Value());
auto state =
fesa::SolidDynamicAnalysisState::Create(domain.Value(), dofs.Value());
auto status = fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
.ExecuteInto(domain.Value(), state.Value());
EXPECT_TRUE(status.IsOk()) << solid_hht_test::Message(status);
if (!status.IsOk()) {
const auto& e = state.Value().IterationRecords().back().evidence;
std::cout << "Failed force metric " << e.residual_metric << " update "
<< e.update_metric << '\n';
return {1, 1, 1};
}
if (count == 30 || compare_parallel) {
// Both element workers have distinct index-owned buffers; compare every
// accepted field, not merely the final displacement norm.
auto parallel_state =
fesa::SolidDynamicAnalysisState::Create(domain.Value(), dofs.Value());
fesa::TbbParallelFor tbb;
fesa::MklPardisoSolver parallel_mass;
fesa::MklPardisoTangentSolver parallel_tangent;
auto parallel_status =
fesa::SolidHhtDynamicProcedureExecutor(tbb, parallel_mass,
parallel_tangent)
.ExecuteInto(domain.Value(), parallel_state.Value());
EXPECT_TRUE(parallel_status.IsOk())
<< solid_hht_test::Message(parallel_status);
if (parallel_status.IsOk()) {
const auto& serial_frames = state.Value().AcceptedFrames();
const auto& parallel_frames = parallel_state.Value().AcceptedFrames();
EXPECT_EQ(serial_frames.size(), parallel_frames.size());
bool has_nonzero_low = false;
for (std::size_t n = 0; n < serial_frames.size(); ++n) {
const auto& sf = serial_frames[n];
const auto& pf = parallel_frames[n];
EXPECT_DOUBLE_EQ(sf.kinetic_energy, pf.kinetic_energy);
EXPECT_DOUBLE_EQ(sf.strain_energy, pf.strain_energy);
EXPECT_DOUBLE_EQ(sf.external_work, pf.external_work);
EXPECT_EQ(sf.angular_momentum, pf.angular_momentum);
EXPECT_EQ(sf.low.strain_energy, pf.low.strain_energy);
EXPECT_EQ(sf.low.kinetic_energy, pf.low.kinetic_energy);
EXPECT_EQ(sf.low.external_work, pf.low.external_work);
EXPECT_EQ(sf.low.energy_balance_error, pf.low.energy_balance_error);
EXPECT_EQ(sf.low.linear_momentum, pf.low.linear_momentum);
EXPECT_EQ(sf.low.angular_momentum, pf.low.angular_momentum);
EXPECT_EQ(sf.low.linear_momentum_rate, pf.low.linear_momentum_rate);
EXPECT_EQ(sf.low.angular_momentum_rate, pf.low.angular_momentum_rate);
EXPECT_EQ(sf.low.global_equilibrium, pf.low.global_equilibrium);
EXPECT_EQ(sf.low.linearization_current_internal_moment,
pf.low.linearization_current_internal_moment);
EXPECT_EQ(sf.low.reference_internal_moment,
pf.low.reference_internal_moment);
EXPECT_EQ(sf.low.current_free_residual_moment,
pf.low.current_free_residual_moment);
for (auto member : {&fesa::SolidDynamicFrameLowParts::displacement,
&fesa::SolidDynamicFrameLowParts::velocity,
&fesa::SolidDynamicFrameLowParts::acceleration,
&fesa::SolidDynamicFrameLowParts::algorithmic_acceleration,
&fesa::SolidDynamicFrameLowParts::inertia_force,
&fesa::SolidDynamicFrameLowParts::internal_force,
&fesa::SolidDynamicFrameLowParts::external_force,
&fesa::SolidDynamicFrameLowParts::residual,
&fesa::SolidDynamicFrameLowParts::reaction,
&fesa::SolidDynamicFrameLowParts::residual_scale}) {
const auto& sl = sf.low.*member;
const auto& pl = pf.low.*member;
EXPECT_EQ(sl.Size(), pl.Size());
if (sl.Size() != pl.Size()) continue;
for (std::size_t i = 0; i < sl.Size(); ++i) {
EXPECT_EQ(sl[i], pl[i]);
has_nonzero_low = has_nonzero_low || sl[i] != 0;
}
}
for (std::size_t i = 0; i < sf.displacement.Size(); ++i) {
EXPECT_DOUBLE_EQ(sf.displacement[i], pf.displacement[i]);
EXPECT_DOUBLE_EQ(sf.velocity[i], pf.velocity[i]);
EXPECT_DOUBLE_EQ(sf.acceleration[i], pf.acceleration[i]);
EXPECT_DOUBLE_EQ(sf.algorithmic_acceleration[i],
pf.algorithmic_acceleration[i]);
EXPECT_DOUBLE_EQ(sf.residual[i], pf.residual[i]);
}
for (std::size_t point = 0; point < sf.solid_results.size(); ++point) {
EXPECT_EQ(sf.solid_results[point].cauchy_stress,
pf.solid_results[point].cauchy_stress);
EXPECT_EQ(sf.solid_results[point].strain,
pf.solid_results[point].strain);
}
}
EXPECT_TRUE(has_nonzero_low);
}
}
std::array<double, 3> errors{}, scales{};
const auto exact = Motion(static_cast<double>(count) * h, nonlinear);
const auto& final = state.Value().CommittedState();
for (unsigned node = 0; node < domain.Value().Nodes().size(); ++node) {
const auto u = Apply(exact.b, domain.Value().Nodes()[node].coordinates);
const auto v =
Apply(exact.velocity, domain.Value().Nodes()[node].coordinates);
const auto a =
Apply(exact.acceleration, domain.Value().Nodes()[node].coordinates);
for (unsigned c = 0; c < 3; ++c) {
const std::array<double, 3> truth{u[c], v[c], a[c]};
const std::array<double, 3> actual{final.displacement[3 * node + c],
final.velocity[3 * node + c],
final.acceleration[3 * node + c]};
for (unsigned j = 0; j < 3; ++j) {
errors[j] += (actual[j] - truth[j]) * (actual[j] - truth[j]);
scales[j] += truth[j] * truth[j];
}
}
}
for (unsigned j = 0; j < 3; ++j) errors[j] = std::sqrt(errors[j] / scales[j]);
for (const auto& f : state.Value().AcceptedFrames()) {
const auto mv = ExactMassProduct(domain.Value(), f.velocity);
const auto ma = ExactMassProduct(domain.Value(), f.acceleration);
std::array<double, 3> p{}, angular{}, pdot{}, ldot{}, vv{};
double kinetic = 0, energy_scale = 0, momentum_scale = 0;
double rate_scale = 0, moment_scale = 0, angular_scale = 0, vv_scale = 0;
double free_residual_sum = 0;
for (unsigned n = 0; n < domain.Value().Nodes().size(); ++n) {
auto x = domain.Value().Nodes()[n].coordinates;
std::array<double, 3> nmv{}, nma{}, v{};
for (unsigned c = 0; c < 3; ++c) {
x[c] += f.displacement[3 * n + c];
v[c] = f.velocity[3 * n + c];
nmv[c] = mv[3 * n + c];
nma[c] = ma[3 * n + c];
p[c] += nmv[c];
pdot[c] += nma[c];
kinetic += .5 * v[c] * nmv[c];
energy_scale += .5 * std::abs(v[c] * nmv[c]);
momentum_scale += std::abs(nmv[c]);
rate_scale += std::abs(nma[c]) + std::abs(f.internal_force[3 * n + c]) +
std::abs(f.external_force[3 * n + c]) +
std::abs(f.reaction[3 * n + c]);
free_residual_sum +=
std::abs(f.residual[3 * n + c] - f.reaction[3 * n + c]);
}
double arm = 0, velocity_scale = 0, node_mv = 0, node_rate = 0;
for (unsigned c = 0; c < 3; ++c) {
arm +=
std::abs(x[c]) + std::abs(domain.Value().Nodes()[n].coordinates[c]);
velocity_scale += std::abs(v[c]);
node_mv += std::abs(nmv[c]);
node_rate += std::abs(nma[c]) + std::abs(f.internal_force[3 * n + c]) +
std::abs(f.external_force[3 * n + c]) +
std::abs(f.reaction[3 * n + c]);
}
angular_scale += arm * node_mv;
moment_scale += arm * node_rate;
vv_scale += velocity_scale * node_mv;
AddCross(x, nmv, &angular);
AddCross(x, nma, &ldot);
AddCross(v, nmv, &vv);
}
const double roundoff = 512 * std::numeric_limits<double>::epsilon();
EXPECT_NEAR(f.kinetic_energy, kinetic, roundoff * energy_scale);
for (unsigned c = 0; c < 3; ++c) {
EXPECT_NEAR(f.linear_momentum[c], p[c], roundoff * momentum_scale);
EXPECT_NEAR(f.angular_momentum[c], angular[c], roundoff * angular_scale);
EXPECT_NEAR(f.linear_momentum_rate[c], pdot[c], roundoff * rate_scale);
EXPECT_NEAR(f.angular_momentum_rate[c], ldot[c], roundoff * moment_scale);
EXPECT_NEAR(vv[c], 0, roundoff * vv_scale);
EXPECT_NEAR(f.global_equilibrium[c], 0,
free_residual_sum + roundoff * rate_scale);
const double identity = f.reference_internal_moment[c] +
f.linearization_current_internal_moment[c] -
f.current_free_residual_moment[c];
EXPECT_NEAR(f.global_equilibrium[3 + c], identity,
roundoff * moment_scale);
if (nonlinear)
EXPECT_NEAR(f.reference_internal_moment[c] +
f.linearization_current_internal_moment[c],
0, roundoff * moment_scale);
else
EXPECT_NEAR(f.reference_internal_moment[c], 0, roundoff * moment_scale);
}
}
return errors;
}
} // namespace
TEST(SolidHhtTemporal, PairedSerialAndTbbPreserveEveryLowComponent) {
for (bool nonlinear : {false, true}) {
SCOPED_TRACE(nonlinear);
ManufacturedRun(.02, 2, nonlinear, true);
}
}
TEST(SolidHhtTemporal, FreeConstantForceHasAnalyticalInitialAcceleration) {
auto d = solid_hht_test::Brick();
d.dynamic_step->boundaries = {{"ALL", 2, 3, 0, {}}};
d.dynamic_step->loads = {{"ALL", 1, 0.25, {}}};
auto domain = fesa::Domain::Create(std::move(d));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
auto result = fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
.Execute(domain.Value());
ASSERT_TRUE(result.HasValue()) << solid_hht_test::Message(result.GetStatus());
const auto& initial = result.Value().state.AcceptedFrames().front();
// Brick mass row sum rho V/8 = 1/4; rigid translation has zero stiffness.
for (unsigned node = 0; node < 8; ++node) {
EXPECT_NEAR(initial.acceleration[3 * node], 1, 1e-13);
EXPECT_DOUBLE_EQ(initial.displacement[3 * node], 0);
}
}
TEST(SolidHhtTemporal,
AllFreeUniformForceRetainsExactRigidTranslationBehavior) {
auto d = solid_hht_test::Brick();
d.dynamic_step->loads = {{"ALL", 1, .25, {}}};
auto domain = fesa::Domain::Create(std::move(d));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
auto model = fesa::AnalysisModel::Create(domain.Value());
auto dofs = fesa::DofManager::Create(model.Value());
auto state =
fesa::SolidDynamicAnalysisState::Create(domain.Value(), dofs.Value());
auto status = fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
.ExecuteInto(domain.Value(), state.Value());
if (!status.IsOk()) {
const auto& e = state.Value().IterationRecords().back().evidence;
std::cout << "Rigid-force failure worst DOF " << e.worst_full_dof
<< " force ratio " << e.residual_metric
<< " terms r,Ma,f,F,scale ";
for (auto term : e.worst_force_terms) std::cout << term << ' ';
std::cout << '\n';
for (const auto& rec : state.Value().IterationRecords()) {
if (rec.evidence.iteration_index < 10) {
std::cout << "Trace " << rec.increment_index << ' '
<< rec.evidence.iteration_index << ' ' << rec.outcome
<< " ratio=" << rec.evidence.residual_metric
<< " worst=" << rec.evidence.worst_full_dof
<< " correction=" << rec.evidence.correction_metrics[0]
<< '\n';
}
}
}
ASSERT_TRUE(status.IsOk()) << solid_hht_test::Message(status);
EXPECT_TRUE(std::any_of(
state.Value().IterationRecords().begin(),
state.Value().IterationRecords().end(),
[](const auto& record) { return record.outcome == "paired-candidate"; }));
const double roundoff = 512 * std::numeric_limits<double>::epsilon();
for (const auto& f : state.Value().AcceptedFrames())
for (unsigned n = 0; n < 8; ++n) {
EXPECT_NEAR(f.displacement[3 * n], .5 * f.time * f.time,
roundoff * .5 * f.time * f.time);
EXPECT_NEAR(f.velocity[3 * n], f.time, roundoff * f.time);
EXPECT_NEAR(f.acceleration[3 * n], 1, roundoff);
for (unsigned c = 1; c < 3; ++c) {
EXPECT_NEAR(f.displacement[3 * n + c], 0,
roundoff * .5 * f.time * f.time);
EXPECT_NEAR(f.velocity[3 * n + c], 0, roundoff * f.time);
EXPECT_NEAR(f.acceleration[3 * n + c], 0, roundoff);
}
}
}
TEST(SolidHhtTemporal,
PairedNewtonKeepsAllOriginalComponentsAcrossAxesAndScales) {
const double roundoff = 512 * std::numeric_limits<double>::epsilon();
for (unsigned axis = 0; axis < 3; ++axis)
for (double acceleration : {1e-4, 1.0, 1000.0}) {
SCOPED_TRACE(axis);
SCOPED_TRACE(acceleration);
auto d = solid_hht_test::Brick(.01, 8);
d.dynamic_step->loads = {
{"ALL", static_cast<int>(axis + 1), .25 * acceleration, {}}};
auto domain = fesa::Domain::Create(std::move(d));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
auto dofs = fesa::DofManager::Create(model.Value());
ASSERT_TRUE(dofs.HasValue());
auto state =
fesa::SolidDynamicAnalysisState::Create(domain.Value(), dofs.Value());
ASSERT_TRUE(state.HasValue());
const auto status =
fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
.ExecuteInto(domain.Value(), state.Value());
if (!status.IsOk()) {
const auto& records = state.Value().IterationRecords();
for (const auto& record : records) {
if (record.increment_index != records.back().increment_index ||
(record.evidence.iteration_index > 8 &&
record.evidence.iteration_index < 95))
continue;
std::cout << "Rigid paired Newton trace "
<< record.evidence.iteration_index << ' ' << record.outcome
<< " metric=" << record.evidence.residual_metric
<< " worst=" << record.evidence.worst_full_dof << " terms=";
for (double term : record.evidence.worst_force_terms)
std::cout << term << ' ';
std::cout << " corrections=";
for (double metric : record.evidence.correction_metrics)
std::cout << metric << ' ';
std::cout << '\n';
}
}
ASSERT_TRUE(status.IsOk()) << solid_hht_test::Message(status);
for (const auto& frame : state.Value().AcceptedFrames()) {
EXPECT_LE(frame.iterations.back().residual_metric, 1e-10);
EXPECT_LE(frame.iterations.back().update_metric, 1e-12);
for (unsigned node = 0; node < 8; ++node)
for (unsigned c = 0; c < 3; ++c) {
const double a = c == axis ? acceleration : 0;
EXPECT_NEAR(frame.displacement[3 * node + c],
.5 * a * frame.time * frame.time,
roundoff * .5 * acceleration * frame.time * frame.time);
EXPECT_NEAR(frame.velocity[3 * node + c], a * frame.time,
roundoff * acceleration * frame.time);
EXPECT_NEAR(frame.acceleration[3 * node + c], a,
roundoff * acceleration);
}
}
}
}
TEST(SolidHhtTemporal,
ActualTwoBrickLinearAndRotatingSmallStrainTlAreSecondOrder) {
for (bool nonlinear : {false, true}) {
SCOPED_TRACE(nonlinear);
const auto coarse = ManufacturedRun(.04, 30, nonlinear);
const auto medium = ManufacturedRun(.02, 60, nonlinear);
const auto fine = ManufacturedRun(.01, 120, nonlinear);
for (unsigned j = 0; j < 3; ++j) {
const double p1 = std::log(coarse[j] / medium[j]) / std::log(2.0);
const double p2 = std::log(medium[j] / fine[j]) / std::log(2.0);
std::cout << "Hex8 " << (nonlinear ? "TL" : "linear") << " u/v/a channel "
<< j << " errors " << coarse[j] << ' ' << medium[j] << ' '
<< fine[j] << " orders " << p1 << ' ' << p2 << '\n';
EXPECT_GT(fine[j],
1e-8); // Meaningful truncation interval, away from noise.
EXPECT_LT(coarse[j], .1);
EXPECT_GE(p1, 1.8);
EXPECT_GE(p2, 1.8);
}
}
}
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.
std::array<std::array<double, 3>, 3> errors{};
for (unsigned level = 0; level < 3; ++level) {
const double h = .02 / static_cast<double>(1U << level);
const unsigned count = 32U << level;
auto d = solid_hht_test::Brick(h, count);
for (unsigned n = 1; n < 8; ++n)
d.dynamic_step->boundaries.push_back(
{"N" + std::to_string(n), 1, 3, 0, {}});
d.dynamic_step->loads = {
{"N0", 1, .01, {}}, {"N0", 2, .02, {}}, {"N0", 3, .03, {}}};
auto domain = fesa::Domain::Create(std::move(d));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
auto result =
fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
.Execute(domain.Value());
ASSERT_TRUE(result.HasValue())
<< 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 std::array<double, 2> k{diagonal + 2 * off, diagonal - off};
for (unsigned c = 0; c < 3; ++c) {
std::array<double, 3> truth{};
const std::array<double, 2> force{.02, .01 * (c + 1) - .02};
for (unsigned mode = 0; mode < 2; ++mode) {
const double w = std::sqrt(k[mode] / m);
truth[0] += force[mode] / k[mode] * (1 - std::cos(w * f.time));
truth[1] += force[mode] / (m * w) * std::sin(w * f.time);
truth[2] += force[mode] / m * std::cos(w * f.time);
}
const std::array<double, 3> actual{f.displacement[c], f.velocity[c],
f.acceleration[c]};
for (unsigned j = 0; j < 3; ++j)
errors[level][j] += std::pow(actual[j] - truth[j], 2);
}
}
for (unsigned j = 0; j < 3; ++j) {
const double p1 =
.5 * std::log(errors[0][j] / errors[1][j]) / std::log(2.0);
const double p2 =
.5 * std::log(errors[1][j] / errors[2][j]) / std::log(2.0);
std::cout << "Independent MDOF channel " << j << " orders " << p1 << ' '
<< p2 << '\n';
EXPECT_GE(p1, 1.8);
EXPECT_GE(p2, 1.8);
EXPECT_GT(errors[2][j], 1e-15);
}
}
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 w = std::sqrt(k / m);
for (unsigned level = 0; level < 3; ++level) {
const double h = .02 / static_cast<double>(1U << level);
auto d = solid_hht_test::Brick(h, 32U << level);
d.dynamic_step->boundaries.push_back({"N0", 2, 3, 0, {}});
for (unsigned n = 1; n < 8; ++n)
d.dynamic_step->boundaries.push_back(
{"N" + std::to_string(n), 1, 3, 0, {}});
d.dynamic_step->loads = {{"N0", 1, .01, {}}};
auto domain = fesa::Domain::Create(std::move(d));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
auto result =
fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
.Execute(domain.Value());
ASSERT_TRUE(result.HasValue())
<< solid_hht_test::Message(result.GetStatus());
const auto& f = result.Value().state.CommittedState();
const std::array<double, 3> truth{.01 / k * (1 - std::cos(w * f.time)),
.01 / (m * w) * std::sin(w * f.time),
.01 / m * std::cos(w * f.time)};
const std::array<double, 3> actual{f.displacement[0], f.velocity[0],
f.acceleration[0]};
for (unsigned j = 0; j < 3; ++j)
errors[level][j] = std::abs(actual[j] - truth[j]);
EXPECT_NEAR(
f.kinetic_energy, .5 * m * f.velocity[0] * f.velocity[0],
512 * std::numeric_limits<double>::epsilon() * f.kinetic_energy);
EXPECT_NEAR(f.strain_energy, .5 * k * f.displacement[0] * f.displacement[0],
512 * std::numeric_limits<double>::epsilon() * f.strain_energy);
}
for (unsigned j = 0; j < 3; ++j) {
const double p1 = std::log(errors[0][j] / errors[1][j]) / std::log(2.0);
const double p2 = std::log(errors[1][j] / errors[2][j]) / std::log(2.0);
std::cout << "Independent SDOF channel " << j << " orders " << p1 << ' '
<< p2 << '\n';
EXPECT_GE(p1, 1.8);
EXPECT_GE(p2, 1.8);
EXPECT_GT(errors[2][j], 1e-8);
}
}
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();
fesa::Hex8Element::Coordinates x{};
fesa::ElementDofLayout layout{
{"I", e + 1, std::to_string(e + 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),
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 response = element.Value().EvaluateLinear(u);
ASSERT_TRUE(response.HasValue());
numerical += response.Value().strain_energy;
}
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;
}
}
@@ -0,0 +1,52 @@
#ifndef FESA_TESTS_SOLID_HHT_TEST_FIXTURE_H_
#define FESA_TESTS_SOLID_HHT_TEST_FIXTURE_H_
#include <array>
#include <string>
#include "fesa/elements/hex8_definition.h"
#include "fesa/model/domain.h"
#include "fesa/properties/solid_section.h"
namespace solid_hht_test {
inline fesa::ModelDefinition Brick(double h = 0.01, unsigned count = 20,
bool nonlinear = false) {
fesa::ModelDefinition d;
d.source_path = "solid-hht-test.inp";
const std::array<std::array<double, 3>, 8> x{{{0, 0, 0},
{1, 0, 0},
{1, 1, 0},
{0, 1, 0},
{0, 0, 1},
{1, 0, 1},
{1, 1, 1},
{0, 1, 1}}};
for (unsigned i = 0; i < 8; ++i) {
d.nodes.push_back({{"I", i + 1, std::to_string(i + 1)}, x[i], {}});
d.node_sets.push_back({"N" + std::to_string(i), {}, {i}, {}});
}
d.node_sets.push_back({"ALL", {}, {0, 1, 2, 3, 4, 5, 6, 7}, {}});
d.materials = {{"M", 12.0, 0.25, {}}};
d.densities = {{0, 2.0, {}}};
d.solid_sections.emplace_back(fesa::SourceEntityId{"I", 1, "1"}, "E", 0,
fesa::SourceLocation{});
d.solid_elements.emplace_back(
fesa::SourceEntityId{"I", 8, "8"},
std::array<fesa::EntityIndex, 8>{0, 1, 2, 3, 4, 5, 6, 7}, 0, 0,
fesa::SourceLocation{});
fesa::DynamicStepDefinition step;
step.source_step_name_raw = "Dynamic";
step.source_step_name_canonical = "DYNAMIC";
step.initial_increment = h;
step.time_period = static_cast<double>(count) * h;
step.increment_count = count;
step.maximum_increments = count;
step.nlgeom = nonlinear;
d.dynamic_step = step;
return d;
}
inline std::string Message(const fesa::Status& s) {
return s.Diagnostics().empty() ? "" : s.Diagnostics()[0].message;
}
} // namespace solid_hht_test
#endif