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
+5 -5
View File
@@ -30,7 +30,7 @@ struct Hex8HighLowResponse {
double strain_energy_low{0.0}; double strain_energy_low{0.0};
}; };
/// @brief Implements full-integration C3D8 linear and TL StVK mechanics. /// @brief Implements C3D8 full deviatoric and mean-volume Green energy.
/// @note Reference geometry is owned and immutable; no trial is committed. /// @note Reference geometry is owned and immutable; no trial is committed.
class Hex8Element final : public Element { class Hex8Element final : public Element {
public: public:
@@ -53,14 +53,14 @@ class Hex8Element final : public Element {
/// @brief Returns f=K0*u and infinitesimal tensor strain at eight points. /// @brief Returns f=K0*u and infinitesimal tensor strain at eight points.
Result<Hex8Response> EvaluateLinear(const Vector& displacement) const; Result<Hex8Response> EvaluateLinear(const Vector& displacement) const;
/// @brief Returns one certified StVK trial and its analytic energy Hessian. /// @brief Returns one certified mean-Green TL trial and its energy Hessian.
Result<Hex8Response> EvaluateNonlinear(const Vector& displacement) const; Result<Hex8Response> EvaluateNonlinear(const Vector& displacement) const;
/// @brief Evaluates linear energy/force/Hessian on the complete paired state. /// @brief Evaluates linear energy/force/Hessian on the complete paired state.
/// @note Uses the same reference-relative gradient operator as the paired TL /// @note Uses the same reference-relative gradient operator as the paired TL
/// path. Inputs must have 24 finite normalized high+low translations. /// path. Inputs must have 24 finite normalized high+low translations.
Result<Hex8HighLowResponse> EvaluateLinearHighLow( Result<Hex8HighLowResponse> EvaluateLinearHighLow(
const Vector& high, const Vector& low) const; const Vector& high, const Vector& low) const;
/// @brief Evaluates certified TL StVK mechanics without collapsing low state. /// @brief Evaluates mean-Green TL mechanics without collapsing low state.
/// @note Failure preserves immutable reference data and all caller state. /// @note Failure preserves immutable reference data and all caller state.
Result<Hex8HighLowResponse> EvaluateNonlinearHighLow( Result<Hex8HighLowResponse> EvaluateNonlinearHighLow(
const Vector& high, const Vector& low) const; const Vector& high, const Vector& low) const;
@@ -81,7 +81,7 @@ class Hex8Element final : public Element {
}; };
/// @brief Constructs an owned candidate before reference validation. /// @brief Constructs an owned candidate before reference validation.
Hex8Element(EntityIndex element_index, ElementDofLayout layout, Hex8Element(EntityIndex element_index, ElementDofLayout layout,
Coordinates coordinates, double lambda, double mu, Coordinates coordinates, double kappa, double mu,
std::optional<double> density, SourceLocation location); std::optional<double> density, SourceLocation location);
/// @brief Evaluates without mutating the cached reference or previous values. /// @brief Evaluates without mutating the cached reference or previous values.
Result<Hex8Response> Evaluate(const Vector& displacement, Result<Hex8Response> Evaluate(const Vector& displacement,
@@ -95,7 +95,7 @@ class Hex8Element final : public Element {
EntityIndex element_index_; EntityIndex element_index_;
ElementDofLayout layout_; ElementDofLayout layout_;
Coordinates coordinates_; Coordinates coordinates_;
double lambda_; double kappa_;
double mu_; double mu_;
std::optional<double> density_; std::optional<double> density_;
SourceLocation location_; SourceLocation location_;
+97 -68
View File
@@ -61,12 +61,12 @@ const char* CertificateMessage(hex8_internal::Certificate certificate) {
} // namespace } // namespace
Hex8Element::Hex8Element(EntityIndex index, ElementDofLayout layout, Hex8Element::Hex8Element(EntityIndex index, ElementDofLayout layout,
Coordinates coordinates, double lambda, double mu, Coordinates coordinates, double kappa, double mu,
std::optional<double> density, SourceLocation location) std::optional<double> density, SourceLocation location)
: element_index_{index}, : element_index_{index},
layout_{std::move(layout)}, layout_{std::move(layout)},
coordinates_{coordinates}, coordinates_{coordinates},
lambda_{lambda}, kappa_{kappa},
mu_{mu}, mu_{mu},
density_{density}, density_{density},
location_{std::move(location)} {} location_{std::move(location)} {}
@@ -102,23 +102,12 @@ Result<Hex8Element> Hex8Element::Create(EntityIndex index,
return Result<Hex8Element>::Failure( return Result<Hex8Element>::Failure(
candidate.Failure("Hex8 requires finite E>0 and -1<nu<0.5.")); candidate.Failure("Hex8 requires finite E>0 and -1<nu<0.5."));
candidate.mu_ = e / (2.0 * (1.0 + nu)); candidate.mu_ = e / (2.0 * (1.0 + nu));
candidate.lambda_ = e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu)); candidate.kappa_ = e / (3.0 * (1.0 - 2.0 * nu));
if (!std::isfinite(candidate.mu_) || candidate.mu_ <= 0.0 || if (!std::isfinite(candidate.mu_) || candidate.mu_ <= 0.0 ||
!std::isfinite(candidate.lambda_) || !std::isfinite(candidate.kappa_) || candidate.kappa_ <= 0.0)
!std::isfinite(candidate.lambda_ + 2.0 * candidate.mu_))
return Result<Hex8Element>::Failure( return Result<Hex8Element>::Failure(
candidate.Failure("Hex8 Lame moduli must remain finite with " candidate.Failure("Hex8 shear and direct bulk moduli must remain "
"representable positive shear modulus.")); "finite and representably positive."));
// Validate the rounded moduli actually used by the kernel, not merely the
// positive exact-real E/nu formula: cancellation near nu=-1 can erase bulk.
const double bulk = candidate.lambda_ + (2.0 * candidate.mu_) / 3.0;
const double volumetric_stress =
3.0 * candidate.lambda_ + 2.0 * candidate.mu_;
if (!std::isfinite(bulk) || bulk <= 0.0 ||
!std::isfinite(volumetric_stress) || volumetric_stress <= 0.0)
return Result<Hex8Element>::Failure(
candidate.Failure("Hex8 rounded Lame moduli require finite "
"representable positive bulk stiffness."));
if (density && (!std::isfinite(*density) || *density <= 0.0)) if (density && (!std::isfinite(*density) || *density <= 0.0))
return Result<Hex8Element>::Failure(candidate.Failure( return Result<Hex8Element>::Failure(candidate.Failure(
"Hex8 reference density must be finite and positive.")); "Hex8 reference density must be finite and positive."));
@@ -156,20 +145,43 @@ Result<Hex8Element> Hex8Element::Create(EntityIndex index,
return Result<Hex8Element>::Failure(candidate.Failure( return Result<Hex8Element>::Failure(candidate.Failure(
"Reference gradient or point coordinate is nonfinite.")); "Reference gradient or point coordinate is nonfinite."));
} }
}
// Zero Hessian of the same reference-relative mean-volume energy.
std::array<Triple, 8> integrated_gradients{};
double volume = 0.0;
for (const auto& point : candidate.points_) {
auto gradients = point.gradients;
gradients[0] = {};
for (std::size_t a = 1; a < 8; ++a)
for (std::size_t i = 0; i < 3; ++i) gradients[0][i] -= gradients[a][i];
volume += point.weight;
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t i = 0; i < 3; ++i)
integrated_gradients[a][i] += point.weight * gradients[a][i];
for (std::size_t a = 0; a < 8; ++a) for (std::size_t a = 0; a < 8; ++a)
for (std::size_t b = 0; b < 8; ++b) { for (std::size_t b = 0; b < 8; ++b) {
const auto& ga = point.gradients[a]; const auto& ga = gradients[a];
const auto& gb = point.gradients[b]; const auto& gb = gradients[b];
const double dot = Dot(ga, gb); const double dot = Dot(ga, gb);
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
for (std::size_t k = 0; k < 3; ++k) { for (std::size_t k = 0; k < 3; ++k) {
candidate.stiffness_(3U * a + i, 3U * b + k) += candidate.stiffness_(3U * a + i, 3U * b + k) +=
point.weight * (candidate.lambda_ * ga[i] * gb[k] + point.weight * (-(2.0 / 3.0) * candidate.mu_ * ga[i] * gb[k] +
candidate.mu_ * ga[k] * gb[i] + candidate.mu_ * ga[k] * gb[i] +
(i == k ? candidate.mu_ * dot : 0.0)); (i == k ? candidate.mu_ * dot : 0.0));
} }
} }
} }
if (!std::isfinite(volume) || volume <= 0.0)
return Result<Hex8Element>::Failure(
candidate.Failure("Hex8 reference volume is nonfinite."));
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t b = 0; b < 8; ++b)
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t k = 0; k < 3; ++k)
candidate.stiffness_(3U * a + i, 3U * b + k) +=
(candidate.kappa_ / volume) * integrated_gradients[a][i] *
integrated_gradients[b][k];
for (std::size_t i = 0; i < 24; ++i) for (std::size_t i = 0; i < 24; ++i)
for (std::size_t j = 0; j < 24; ++j) for (std::size_t j = 0; j < 24; ++j)
if (!std::isfinite(candidate.stiffness_(i, j))) if (!std::isfinite(candidate.stiffness_(i, j)))
@@ -278,48 +290,33 @@ Result<Hex8Response> Hex8Element::Evaluate(const Vector& u,
if (!nonlinear) { if (!nonlinear) {
response.material_tangent = stiffness_; response.material_tangent = stiffness_;
response.tangent = stiffness_; response.tangent = stiffness_;
// K0 annihilates uniform translation. Use the same relative field as
// strain recovery to avoid cancellation of a large rigid displacement.
// Retain low product bits and cancelled sum terms in bending-dominated
// rows; the stored stiffness and its tangent are unchanged.
for (std::size_t i = 0; i < 24; ++i) {
double sum = 0.0;
double correction = 0.0;
for (std::size_t j = 0; j < 24; ++j) {
AccumulateProduct(stiffness_(i, j), relative[j], sum, correction);
AccumulateProduct(stiffness_(i, j), relative_correction[j], sum,
correction);
}
response.internal_force[i] = sum + correction;
}
double energy = 0.0;
double energy_correction = 0.0;
for (std::size_t i = 0; i < 24; ++i) {
AccumulateProduct(relative[i], response.internal_force[i], energy,
energy_correction);
AccumulateProduct(relative_correction[i], response.internal_force[i],
energy, energy_correction);
}
response.strain_energy = 0.5 * (energy + energy_correction);
} }
std::array<Tensor, 8> deformations{}, strains{};
std::array<std::array<Triple, 8>, 8> gradients{};
double volume = 0.0, trace_integral = 0.0;
Vector force_correction{24U}; Vector force_correction{24U};
for (std::size_t g = 0; g < 8; ++g) { for (std::size_t g = 0; g < 8; ++g) {
const auto& point = points_[g]; const auto& point = points_[g];
gradients[g] = point.gradients;
Triple sum{};
for (std::size_t a = 1; a < 8; ++a)
for (std::size_t j = 0; j < 3; ++j) sum[j] += gradients[g][a][j];
for (std::size_t j = 0; j < 3; ++j) gradients[g][0][j] = -sum[j];
Tensor gradient{}; Tensor gradient{};
Tensor gradient_correction{}; Tensor gradient_correction{};
for (std::size_t a = 0; a < 8; ++a) for (std::size_t a = 1; a < 8; ++a)
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) { for (std::size_t j = 0; j < 3; ++j) {
AccumulateProduct(relative[3U * a + i], point.gradients[a][j], AccumulateProduct(relative[3U * a + i], point.gradients[a][j],
gradient[i][j], gradient_correction[i][j]); gradient[i][j], gradient_correction[i][j]);
AccumulateProduct(relative_correction[3U * a + i], AccumulateProduct(relative_correction[3U * a + i],
point.gradients[a][j], point.gradients[a][j], gradient[i][j],
gradient[i][j], gradient_correction[i][j]); gradient_correction[i][j]);
} }
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) for (std::size_t j = 0; j < 3; ++j)
gradient[i][j] += gradient_correction[i][j]; gradient[i][j] += gradient_correction[i][j];
Tensor f = gradient; Tensor f = nonlinear ? gradient : Tensor{};
for (std::size_t i = 0; i < 3; ++i) f[i][i] += 1.0; for (std::size_t i = 0; i < 3; ++i) f[i][i] += 1.0;
Tensor strain{}; Tensor strain{};
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
@@ -330,36 +327,60 @@ Result<Hex8Response> Hex8Element::Evaluate(const Vector& u,
for (std::size_t k = 0; k < 3; ++k) for (std::size_t k = 0; k < 3; ++k)
strain[i][j] += 0.5 * gradient[k][i] * gradient[k][j]; strain[i][j] += 0.5 * gradient[k][i] * gradient[k][j];
} }
deformations[g] = f;
strains[g] = strain;
volume += point.weight;
trace_integral +=
point.weight * (strain[0][0] + strain[1][1] + strain[2][2]);
if (!Finite(gradient) || !Finite(f) || !Finite(strain))
return Result<Hex8Response>::Failure(
Failure("Hex8 trial tensor is nonfinite."));
}
const double mean_trace = trace_integral / volume;
if (!std::isfinite(volume) || volume <= 0 || !std::isfinite(mean_trace))
return Result<Hex8Response>::Failure(
Failure("Hex8 mean volume is nonfinite."));
response.strain_energy = (0.5 * kappa_ * mean_trace) * (volume * mean_trace);
std::array<Triple, 8> integrated_fg{};
for (std::size_t g = 0; g < 8; ++g) {
const auto& point = points_[g];
const auto& f = deformations[g];
const auto& strain = strains[g];
const double trace = strain[0][0] + strain[1][1] + strain[2][2]; const double trace = strain[0][0] + strain[1][1] + strain[2][2];
Tensor s{}; Tensor s{}, p{}, fft{};
double density_energy = 0.0;
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) for (std::size_t j = 0; j < 3; ++j) {
s[i][j] = 2.0 * mu_ * strain[i][j] + (i == j ? lambda_ * trace : 0.0); const double dev = strain[i][j] - (i == j ? trace / 3.0 : 0.0);
s[i][j] = 2.0 * mu_ * dev + (i == j ? kappa_ * mean_trace : 0.0);
density_energy += mu_ * dev * dev;
}
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) {
for (std::size_t k = 0; k < 3; ++k) {
p[i][j] += f[i][k] * s[k][j];
fft[i][j] += f[i][k] * f[j][k];
}
}
response.strain_energy += density_energy * point.weight;
Tensor stress = s; Tensor stress = s;
if (nonlinear) { if (nonlinear) {
const double det_f = hex8_internal::Determinant(f); const double det_f = hex8_internal::Determinant(f);
if (!std::isfinite(det_f) || det_f <= 0.0) if (!std::isfinite(det_f) || det_f <= 0.0)
return Result<Hex8Response>::Failure(Failure( return Result<Hex8Response>::Failure(Failure(
"Hex8 trial deformation determinant must be finite and positive.")); "Hex8 trial deformation determinant must be finite and positive."));
Tensor p{};
Tensor fft{};
double energy = 0.5 * lambda_ * trace * trace;
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) {
energy += mu_ * strain[i][j] * strain[i][j];
for (std::size_t k = 0; k < 3; ++k) {
p[i][j] += f[i][k] * s[k][j];
fft[i][j] += f[i][k] * f[j][k];
}
}
response.strain_energy += energy * point.weight;
stress = {}; stress = {};
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) for (std::size_t j = 0; j < 3; ++j)
for (std::size_t k = 0; k < 3; ++k) for (std::size_t k = 0; k < 3; ++k)
stress[i][j] += p[i][k] * f[j][k] / det_f; stress[i][j] += p[i][k] * f[j][k] / det_f;
}
for (std::size_t a = 0; a < 8; ++a) { for (std::size_t a = 0; a < 8; ++a) {
const auto& ga = point.gradients[a]; for (std::size_t i = 0; i < 3; ++i)
integrated_fg[a][i] += point.weight * Dot(f[i], gradients[g][a]);
}
for (std::size_t a = 0; a < 8; ++a) {
const auto& ga = gradients[g][a];
for (std::size_t i = 0; i < 3; ++i) { for (std::size_t i = 0; i < 3; ++i) {
double contraction = 0.0; double contraction = 0.0;
double contraction_correction = 0.0; double contraction_correction = 0.0;
@@ -373,8 +394,9 @@ Result<Hex8Response> Hex8Element::Evaluate(const Vector& u,
response.internal_force[3U * a + i], response.internal_force[3U * a + i],
force_correction[3U * a + i]); force_correction[3U * a + i]);
} }
if (nonlinear)
for (std::size_t b = 0; b < 8; ++b) { for (std::size_t b = 0; b < 8; ++b) {
const auto& gb = point.gradients[b]; const auto& gb = gradients[g][b];
double geometric = 0.0; double geometric = 0.0;
for (std::size_t j = 0; j < 3; ++j) for (std::size_t j = 0; j < 3; ++j)
for (std::size_t l = 0; l < 3; ++l) for (std::size_t l = 0; l < 3; ++l)
@@ -384,7 +406,8 @@ Result<Hex8Response> Hex8Element::Evaluate(const Vector& u,
const auto row = 3U * a + i; const auto row = 3U * a + i;
const auto column = 3U * b + k; const auto column = 3U * b + k;
response.material_tangent(row, column) += response.material_tangent(row, column) +=
point.weight * (lambda_ * Dot(f[i], ga) * Dot(f[k], gb) + point.weight *
(-(2.0 / 3.0) * mu_ * Dot(f[i], ga) * Dot(f[k], gb) +
mu_ * Dot(f[i], gb) * Dot(f[k], ga) + mu_ * Dot(f[i], gb) * Dot(f[k], ga) +
mu_ * fft[i][k] * Dot(ga, gb)); mu_ * fft[i][k] * Dot(ga, gb));
if (i == k) if (i == k)
@@ -393,17 +416,23 @@ Result<Hex8Response> Hex8Element::Evaluate(const Vector& u,
} }
} }
} }
} if (!Finite(f) || !Finite(strain) || !Finite(s) || !Finite(p) ||
if (!Finite(f) || !Finite(strain) || !Finite(s) || !Finite(stress)) !Finite(fft) || !Finite(stress))
return Result<Hex8Response>::Failure( return Result<Hex8Response>::Failure(
Failure("Hex8 trial tensor is nonfinite.")); Failure("Hex8 trial tensor is nonfinite."));
response.points[g] = {element_index_, static_cast<int>(g + 1U), response.points[g] = {element_index_, static_cast<int>(g + 1U),
point.natural, point.position, point.natural, point.position,
Pack(stress), Pack(strain)}; Pack(stress), Pack(strain)};
} }
if (nonlinear)
for (std::size_t i = 0; i < 24; ++i) for (std::size_t i = 0; i < 24; ++i)
response.internal_force[i] += force_correction[i]; response.internal_force[i] += force_correction[i];
if (nonlinear)
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t b = 0; b < 8; ++b)
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t k = 0; k < 3; ++k)
response.material_tangent(3 * a + i, 3 * b + k) +=
(kappa_ / volume) * integrated_fg[a][i] * integrated_fg[b][k];
if (!std::isfinite(response.strain_energy)) if (!std::isfinite(response.strain_energy))
return Result<Hex8Response>::Failure( return Result<Hex8Response>::Failure(
Failure("Hex8 trial energy is nonfinite.")); Failure("Hex8 trial energy is nonfinite."));
+57 -9
View File
@@ -86,7 +86,10 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
Hex8HighLowResponse response; Hex8HighLowResponse response;
std::array<HighLow, 24> force{}; std::array<HighLow, 24> force{};
std::array<std::array<HighLow, 24>, 24> material{}, geometric{}; std::array<std::array<HighLow, 24>, 24> material{}, geometric{};
HighLow energy; HighLow energy, volume, trace_integral;
std::array<Tensor, 8> deformations{}, strains{};
std::array<std::array<Triple, 8>, 8> point_gradients{};
std::array<Triple, 8> integrated_fg{};
for (std::size_t g = 0; g < 8; ++g) { for (std::size_t g = 0; g < 8; ++g) {
const auto& point = points_[g]; const auto& point = points_[g];
std::array<Triple, 8> gradients{}; std::array<Triple, 8> gradients{};
@@ -104,7 +107,7 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
for (std::size_t j = 0; j < 3; ++j) for (std::size_t j = 0; j < 3; ++j)
gradients[0][j] = math_internal::Negate(gradient_sum[j]); gradients[0][j] = math_internal::Negate(gradient_sum[j]);
Tensor f{}; Tensor f{};
Tensor strain{}, s{}, p{}, fft{}; Tensor strain{};
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) { for (std::size_t j = 0; j < 3; ++j) {
f[i][j] = nonlinear ? h[i][j] : HighLow{}; f[i][j] = nonlinear ? h[i][j] : HighLow{};
@@ -114,13 +117,48 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
for (std::size_t k = 0; k < 3; ++k) for (std::size_t k = 0; k < 3; ++k)
strain[i][j] += HighLow{0.5} * h[k][i] * h[k][j]; strain[i][j] += HighLow{0.5} * h[k][i] * h[k][j];
} }
if (!Finite(h) || !Finite(f) || !Finite(strain))
return Result<Hex8HighLowResponse>::Failure(Failure(
"Hex8 high+low strain arithmetic is outside its supported range."));
point_gradients[g] = gradients;
deformations[g] = f;
strains[g] = strain;
volume += HighLow{point.weight};
trace_integral +=
HighLow{point.weight} * (strain[0][0] + strain[1][1] + strain[2][2]);
}
if (!IsNormalized(volume) || math_internal::Compare(volume, HighLow{}) != 1 ||
!IsNormalized(trace_integral))
return Result<Hex8HighLowResponse>::Failure(
Failure("Hex8 high+low reference volume or trace is unsupported."));
const HighLow mean_trace = trace_integral / volume;
if (!IsNormalized(mean_trace))
return Result<Hex8HighLowResponse>::Failure(
Failure("Hex8 high+low weighted mean is unsupported."));
const HighLow twice_mu = HighLow{2} * HighLow{mu_};
const HighLow deviatoric_coefficient =
math_internal::Negate(twice_mu) / HighLow{3};
const HighLow bulk_coefficient = HighLow{kappa_} / volume;
if (!IsNormalized(twice_mu) || !IsNormalized(deviatoric_coefficient) ||
!IsNormalized(bulk_coefficient))
return Result<Hex8HighLowResponse>::Failure(
Failure("Hex8 high+low constitutive coefficients are unsupported."));
energy = HighLow{0.5} * HighLow{kappa_} * volume * mean_trace * mean_trace;
for (std::size_t g = 0; g < 8; ++g) {
const auto& point = points_[g];
const auto& gradients = point_gradients[g];
const auto& f = deformations[g];
const auto& strain = strains[g];
const HighLow trace = strain[0][0] + strain[1][1] + strain[2][2]; const HighLow trace = strain[0][0] + strain[1][1] + strain[2][2];
HighLow density_energy = HighLow{0.5} * HighLow{lambda_} * trace * trace; Tensor s{}, p{}, fft{};
HighLow density_energy;
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) { for (std::size_t j = 0; j < 3; ++j) {
s[i][j] = HighLow{2} * HighLow{mu_} * strain[i][j]; const HighLow dev =
if (i == j) s[i][j] += HighLow{lambda_} * trace; strain[i][j] - (i == j ? trace / HighLow{3} : HighLow{});
density_energy += HighLow{mu_} * strain[i][j] * strain[i][j]; s[i][j] = twice_mu * dev;
if (i == j) s[i][j] += HighLow{kappa_} * mean_trace;
density_energy += HighLow{mu_} * dev * dev;
} }
energy += density_energy * HighLow{point.weight}; energy += density_energy * HighLow{point.weight};
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
@@ -144,8 +182,8 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
stress[i][j] = stress[i][j] / det; stress[i][j] = stress[i][j] / det;
} }
} }
if (!Finite(h) || !Finite(f) || !Finite(strain) || !Finite(s) || if (!Finite(f) || !Finite(strain) || !Finite(s) || !Finite(p) ||
!Finite(p) || !Finite(fft) || !Finite(stress)) !Finite(fft) || !Finite(stress))
return Result<Hex8HighLowResponse>::Failure(Failure( return Result<Hex8HighLowResponse>::Failure(Failure(
"Hex8 high+low tensor arithmetic is outside its supported range.")); "Hex8 high+low tensor arithmetic is outside its supported range."));
response.high.points[g] = {element_index_, static_cast<int>(g + 1U), response.high.points[g] = {element_index_, static_cast<int>(g + 1U),
@@ -160,6 +198,9 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
for (std::size_t a = 0; a < 8; ++a) for (std::size_t a = 0; a < 8; ++a)
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
fg[a][i] = Dot(f[i], gradients[a]); fg[a][i] = Dot(f[i], gradients[a]);
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t i = 0; i < 3; ++i)
integrated_fg[a][i] += fg[a][i] * HighLow{point.weight};
for (std::size_t a = 0; a < 8; ++a) for (std::size_t a = 0; a < 8; ++a)
for (std::size_t b = 0; b < 8; ++b) { for (std::size_t b = 0; b < 8; ++b) {
const auto dot = Dot(gradients[a], gradients[b]); const auto dot = Dot(gradients[a], gradients[b]);
@@ -172,7 +213,7 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
for (std::size_t k = 0; k < 3; ++k) { for (std::size_t k = 0; k < 3; ++k) {
const auto row = 3U * a + i; const auto row = 3U * a + i;
const auto column = 3U * b + k; const auto column = 3U * b + k;
const auto mat = HighLow{lambda_} * fg[a][i] * fg[b][k] + const auto mat = deviatoric_coefficient * fg[a][i] * fg[b][k] +
HighLow{mu_} * fg[b][i] * fg[a][k] + HighLow{mu_} * fg[b][i] * fg[a][k] +
HighLow{mu_} * fft[i][k] * dot; HighLow{mu_} * fft[i][k] * dot;
material[row][column] += mat * HighLow{point.weight}; material[row][column] += mat * HighLow{point.weight};
@@ -181,6 +222,13 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
} }
} }
} }
if (nonlinear || !paired_linear_tangent_ready_)
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t b = 0; b < 8; ++b)
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t k = 0; k < 3; ++k)
material[3 * a + i][3 * b + k] +=
bulk_coefficient * integrated_fg[a][i] * integrated_fg[b][k];
if (!IsNormalized(energy)) if (!IsNormalized(energy))
return Result<Hex8HighLowResponse>::Failure( return Result<Hex8HighLowResponse>::Failure(
Failure("Hex8 high+low energy is unsupported or nonfinite.")); Failure("Hex8 high+low energy is unsupported or nonfinite."));
@@ -99,8 +99,11 @@ std::vector<double> DenseOracle(const std::vector<std::size_t>& free) {
for (std::size_t d = 0; d < 3; ++d) for (std::size_t d = 0; d < 3; ++d)
gradient_dot += GradientIntegral(a, b, d, d); gradient_dot += GradientIntegral(a, b, d, d);
k[row * n + col] = k[row * n + col] =
48.0 * (GradientIntegral(a, b, i, j) + GradientIntegral(a, b, j, i) + 48.0 *
(i == j ? gradient_dot : 0.0)); (-(2.0 / 3.0) * GradientIntegral(a, b, i, j) +
GradientIntegral(a, b, j, i) + (i == j ? gradient_dot : 0.0)) +
(80.0 / 48.0) * (48.0 * kSigns[a][i] / (8.0 * (i + 1))) *
(48.0 * kSigns[b][j] / (8.0 * (j + 1)));
if (i == j) { if (i == j) {
m[row * n + col] = 2.0 * 48.0 / 64.0; m[row * n + col] = 2.0 * 48.0 / 64.0;
for (std::size_t d = 0; d < 3; ++d) for (std::size_t d = 0; d < 3; ++d)
@@ -573,7 +573,7 @@ TEST(SolidHhtDynamics,
auto result = RunDynamics(OneFree()); auto result = RunDynamics(OneFree());
ASSERT_TRUE(result.HasValue()) << solid_hht_test::Message(result.GetStatus()); ASSERT_TRUE(result.HasValue()) << solid_hht_test::Message(result.GetStatus());
const auto d = solid_hht_test::Brick(); const auto d = solid_hht_test::Brick();
constexpr double lambda = 4.8; constexpr double bulk = 8.0;
constexpr double mu = 4.8; constexpr double mu = 4.8;
const double roundoff = 512 * std::numeric_limits<double>::epsilon(); const double roundoff = 512 * std::numeric_limits<double>::epsilon();
const auto& frames = result.Value().state.AcceptedFrames(); const auto& frames = result.Value().state.AcceptedFrames();
@@ -592,17 +592,20 @@ TEST(SolidHhtDynamics,
double stiffness; double stiffness;
double stiffness_term_scale; double stiffness_term_scale;
if (c == 0) { if (c == 0) {
stiffness = (lambda + 2 * mu) * (-sign[0]) * product[1] * product[2] + stiffness = (4 * mu / 3) * (-sign[0]) * product[1] * product[2] +
mu * (-sign[1]) * product[0] * product[2] + mu * (-sign[1]) * product[0] * product[2] +
mu * (-sign[2]) * product[0] * product[1]; mu * (-sign[2]) * product[0] * product[1];
stiffness_term_scale = (lambda + 2 * mu) * product[1] * product[2] + stiffness_term_scale = (4 * mu / 3) * product[1] * product[2] +
mu * product[0] * product[2] + mu * product[0] * product[2] +
mu * product[0] * product[1]; mu * product[0] * product[1];
} else { } else {
const unsigned other = c == 1 ? 2 : 1; const unsigned other = c == 1 ? 2 : 1;
stiffness = -.25 * (lambda * sign[c] + mu * sign[0]) * product[other]; stiffness =
stiffness_term_scale = .25 * (lambda + mu) * product[other]; -.25 * ((-2 * mu / 3) * sign[c] + mu * sign[0]) * product[other];
stiffness_term_scale = .25 * (2 * mu / 3 + mu) * product[other];
} }
stiffness -= bulk * sign[c] / 16;
stiffness_term_scale += bulk / 16;
const double inertia = c == 0 ? scalar_mass * f.acceleration[0] : 0; const double inertia = c == 0 ? scalar_mass * f.acceleration[0] : 0;
const double internal = stiffness * f.displacement[0]; const double internal = stiffness * f.displacement[0];
const double applied = b == 0 && c == 0 ? .01 : 0; const double applied = b == 0 && c == 0 ? .01 : 0;
@@ -24,7 +24,7 @@ AffineMotion Motion(double time, bool nonlinear) {
const double dq = 2 * std::sin(2 * time); const double dq = 2 * std::sin(2 * time);
const double ddq = 4 * std::cos(2 * time); const double ddq = 4 * std::cos(2 * time);
AffineMotion m; AffineMotion m;
constexpr double lambda = 4.8; constexpr double bulk = 8;
constexpr double mu = 4.8; constexpr double mu = 4.8;
if (!nonlinear) { if (!nonlinear) {
const M3 d{{{.015, .008, -.004}, {.006, -.01, .003}, {.005, .004, .012}}}; 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.b[i][j] = q * d[i][j];
m.velocity[i][j] = dq * d[i][j]; m.velocity[i][j] = dq * d[i][j];
m.acceleration[i][j] = ddq * d[i][j]; m.acceleration[i][j] = ddq * d[i][j];
m.piola[i][j] = m.piola[i][j] = (i == j ? (bulk * trace - 2 * mu * trace / 3) : 0) +
(i == j ? lambda * trace : 0) + mu * q * (d[i][j] + d[j][i]); mu * q * (d[i][j] + d[j][i]);
} }
} else { } else {
const double s = .004 * q, ds = .004 * dq, dds = .004 * ddq; 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 jr{{{-sn, -c, 0}, {c, -sn, 0}, {0, 0, 0}}};
const M3 jjr{{{-c, sn, 0}, {-sn, -c, 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 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 i = 0; i < 3; ++i)
for (unsigned j = 0; j < 3; ++j) { for (unsigned j = 0; j < 3; ++j) {
m.b[i][j] = (1 + s) * r[i][j] - (i == j ? 1 : 0); m.b[i][j] = (1 + s) * r[i][j] - (i == j ? 1 : 0);
@@ -506,7 +506,7 @@ TEST(SolidHhtTemporal,
TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) { TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
// Node0's three translations are free. Exact polynomial integrals yield // 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{}; std::array<std::array<double, 3>, 3> errors{};
for (unsigned level = 0; level < 3; ++level) { for (unsigned level = 0; level < 3; ++level) {
const double h = .02 / static_cast<double>(1U << level); const double h = .02 / static_cast<double>(1U << level);
@@ -529,8 +529,8 @@ TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
<< solid_hht_test::Message(result.GetStatus()); << solid_hht_test::Message(result.GetStatus());
const auto& f = result.Value().state.CommittedState(); const auto& f = result.Value().state.CommittedState();
const double m = 2.0 / 27; const double m = 2.0 / 27;
const double diagonal = (4.8 + 4 * 4.8) / 9; const double diagonal = 41.0 / 18;
const double off = (4.8 + 4.8) / 12; const double off = 19.0 / 30;
const std::array<double, 2> k{diagonal + 2 * off, diagonal - off}; const std::array<double, 2> k{diagonal + 2 * off, diagonal - off};
for (unsigned c = 0; c < 3; ++c) { for (unsigned c = 0; c < 3; ++c) {
std::array<double, 3> truth{}; std::array<double, 3> truth{};
@@ -563,7 +563,7 @@ TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
TEST(SolidHhtTemporal, IndependentScalarBrickOscillatorHasSecondOrderUVA) { TEST(SolidHhtTemporal, IndependentScalarBrickOscillatorHasSecondOrderUVA) {
std::array<std::array<double, 3>, 3> errors{}; std::array<std::array<double, 3>, 3> errors{};
const double m = 2.0 / 27; 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); const double w = std::sqrt(k / m);
for (unsigned level = 0; level < 3; ++level) { for (unsigned level = 0; level < 3; ++level) {
const double h = .02 / static_cast<double>(1U << level); const double h = .02 / static_cast<double>(1U << level);
@@ -608,46 +608,95 @@ TEST(SolidHhtTemporal, IndependentScalarBrickOscillatorHasSecondOrderUVA) {
} }
} }
TEST(SolidHhtTemporal, namespace {
BendingMeshRefinementQuantifiesFullIntegrationShearLocking) { // Compatible nodal pure-bending field; no equilibrium solve is claimed.
constexpr double thickness = .125; std::array<double, 3> BendingEnergy(unsigned nx, unsigned ny) {
constexpr double curvature = .01; constexpr double thickness = .125, curvature = .01, bulk = 8;
constexpr double lambda = 4.8; std::array<double, 3> result{};
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(); 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::Hex8Element::Coordinates x{};
fesa::ElementDofLayout layout{ 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}, {0, 1, 2, 3, 4, 5, 6, 7},
{fesa::DofComponent::kUx, fesa::DofComponent::kUy, {fesa::DofComponent::kUx, fesa::DofComponent::kUy,
fesa::DofComponent::kUz}}; fesa::DofComponent::kUz}};
fesa::Vector u(24); fesa::Vector u(24);
for (unsigned a = 0; a < 8; ++a) { for (unsigned a = 0; a < 8; ++a) {
x[a] = {static_cast<double>(e) / nx + d.nodes[a].coordinates[0] / nx, x[a] = {(ex + d.nodes[a].coordinates[0]) / nx,
thickness * (d.nodes[a].coordinates[1] - .5), thickness * ((ey + d.nodes[a].coordinates[1]) / ny - .5),
d.nodes[a].coordinates[2]}; d.nodes[a].coordinates[2]};
u[3 * a] = -curvature * x[a][0] * x[a][1]; u[3 * a] = -curvature * x[a][0] * x[a][1];
u[3 * a + 1] = .5 * curvature * x[a][0] * x[a][0]; u[3 * a + 1] = .5 * curvature * x[a][0] * x[a][0];
} }
auto element = fesa::Hex8Element::Create(e, layout, x, d.materials[0], 2); auto element =
ASSERT_TRUE(element.HasValue()); fesa::Hex8Element::Create(index, layout, x, d.materials[0], 2);
EXPECT_TRUE(element.HasValue());
if (!element.HasValue()) return {};
auto response = element.Value().EvaluateLinear(u); auto response = element.Value().EvaluateLinear(u);
ASSERT_TRUE(response.HasValue()); EXPECT_TRUE(response.HasValue());
numerical += response.Value().strain_energy; 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 error = numerical / continuum - 1; const double volumetric =
const double exact_parasitic = .5 * bulk * trace_integral * trace_integral / volume;
mu / (lambda + 2 * mu) / std::pow(nx * thickness, 2); result[0] += response.Value().strain_energy;
EXPECT_NEAR(error, exact_parasitic, 2e-11); result[1] += response.Value().strain_energy - volumetric;
if (previous_error != 0) EXPECT_NEAR(previous_error / error, 4, 1e-10); result[2] += volumetric;
std::cout << "Bending nx=" << nx }
<< " energy/continuum=" << numerical / continuum << '\n'; return result;
previous_error = error; }
} // 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';
} }
} }
+227 -13
View File
@@ -11,6 +11,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "../../../src/fesa/math/high_low_arithmetic.h"
namespace { namespace {
using Coordinates = fesa::Hex8Element::Coordinates; using Coordinates = fesa::Hex8Element::Coordinates;
constexpr int kSigns[8][3] = {{-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, 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; scalar *= kSigns[a][d] == kSigns[b][d] ? 2.0 : 1.0;
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) { 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); 48.0 * GradientProduct(a, b, j, i);
if (i == j) if (i == j)
for (std::size_t d = 0; d < 3; ++d) for (std::size_t d = 0; d < 3; ++d)
exact += 48.0 * GradientProduct(a, b, d, 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(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); 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()); ASSERT_TRUE(trial.HasValue());
const double derivative = direction.Dot(trial.Value().internal_force); const double derivative = direction.Dot(trial.Value().internal_force);
const auto tangent_direction = trial.Value().tangent.Multiply(direction); const auto tangent_direction = trial.Value().tangent.Multiply(direction);
std::array<double, 6> energy_error{}, force_error{}; std::array<double, 8> energy_error{}, force_error{};
const std::array<double, 6> steps{1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7}; 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) { for (std::size_t s = 0; s < steps.size(); ++s) {
auto plus = u, minus = u; auto plus = u, minus = u;
plus.Axpy(steps[s], direction); plus.Axpy(steps[s], direction);
@@ -751,14 +757,14 @@ TEST(Hex8Element, RejectsUnrepresentableShearAndMassInsteadOfReturningZeros) {
ASSERT_TRUE(element.HasValue()); ASSERT_TRUE(element.HasValue());
EXPECT_FALSE(element.Value().ComputeMass().HasValue()); EXPECT_FALSE(element.Value().ComputeMass().HasValue());
} }
TEST(Hex8Element, RejectsRoundedLossOfPositiveVolumetricModulus) { TEST(Hex8Element, DirectBulkSurvivesNearNegativePoissonBound) {
// 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.
const double nu = std::nextafter(-1.0, 0.0); const double nu = std::nextafter(-1.0, 0.0);
auto element = Make(Brick(), 1.0, nu); auto element = Make(Brick(), 1.0, nu);
ASSERT_FALSE(element.HasValue()); ASSERT_TRUE(element.HasValue());
EXPECT_NE(element.GetStatus().Diagnostics()[0].message.find("bulk"), // Direct kappa is positive even where a lambda-based difference loses it.
std::string::npos); 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) { TEST(Hex8Element, OrdinaryNegativePoissonRatioRetainsPositiveVolumetricEnergy) {
auto element = Make(Brick(), 120.0, -.5); 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]}; a[0] * b[1] - a[1] * b[0]};
} }
std::array<double, 2> WarpedQuadratureEnergies(const fesa::Vector& u, std::array<double, 2> WarpedQuadratureEnergies(const fesa::Vector& u,
unsigned count) { unsigned count,
bool nonlinear = false) {
const auto rule = LegendreRule(count); const auto rule = LegendreRule(count);
std::array<double, 2> result{}; std::array<double, 2> result{};
double volume = 0.0, trace_integral = 0.0;
for (const auto& xi : rule) for (const auto& xi : rule)
for (const auto& eta : rule) for (const auto& eta : rule)
for (const auto& zeta : 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 j = 0; j < 3; ++j)
for (std::size_t d = 0; d < 3; ++d) for (std::size_t d = 0; d < 3; ++d)
gradient[i][j] += natural_gradient[i][d] * reciprocal[d][j]; 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; double trace = 0.0, strain_squared = 0.0, velocity_squared = 0.0;
for (std::size_t i = 0; i < 3; ++i) { for (std::size_t i = 0; i < 3; ++i) {
trace += gradient[i][i]; trace += strains[i][i];
velocity_squared += velocity[i] * velocity[i]; velocity_squared += velocity[i] * velocity[i];
for (std::size_t j = 0; j < 3; ++j) { 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; strain_squared += strain * strain;
} }
} }
const double weight = xi[1] * eta[1] * zeta[1] * det; 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[1] += weight * velocity_squared; // .5*rho*v^2 with rho=2.
} }
result[0] += 40.0 * trace_integral * trace_integral / volume;
return result; return result;
} }
TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) { TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) {
@@ -922,4 +941,199 @@ TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) {
<< " K3-vs-K7=" << std::abs(q3[0] - q7[0]) / q7[0] << " K3-vs-K7=" << std::abs(q3[0] - q7[0]) / q7[0]
<< " K5-vs-K7=" << std::abs(q5[0] - q7[0]) / q7[0] << '\n'; << " 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 } // namespace
@@ -295,4 +295,40 @@ TEST(Hex8HighLow, FiniteRotationRetainsTinyShearForceCovariance) {
EXPECT_LE(std::abs(Subtract(rotated[2], baseline[2]).high), 1e-13*gamma); EXPECT_LE(std::abs(Subtract(rotated[2], baseline[2]).high), 1e-13*gamma);
} }
} }
TEST(Hex8HighLow,
MeanVolumeLowOnlyNonuniformTraceRetainsMeanAndCrossPointHessian) {
auto brick = UnitBrick(0, .25);
fesa::Vector high(24), low(24), zero(24);
const double c = std::ldexp(1.0, -60);
for (std::size_t n = 0; n < 8; ++n) high[3 * n] = 1;
low[6] = c;
low[18] = c;
const double coefficient = 7 * 4.8 / 18 + 8.0 / 8;
const auto response = brick.EvaluateLinearHighLow(high, low);
const auto high_only = brick.EvaluateLinearHighLow(high, zero);
ASSERT_TRUE(response.HasValue());
ASSERT_TRUE(high_only.HasValue());
EXPECT_EQ(high_only.Value().high.strain_energy, 0);
EXPECT_NEAR(response.Value().high.strain_energy, coefficient * c * c,
1e-13 * c * c);
EXPECT_NEAR(response.Value().high.internal_force[6] +
response.Value().high.internal_force[18],
2 * coefficient * c, 1e-13 * c);
for (const auto& point : response.Value().high.points) {
const auto& s = point.cauchy_stress;
EXPECT_NEAR((s[0] + s[1] + s[2]) / 3, 4 * c, 1e-13 * c);
EXPECT_NEAR(point.strain[0], c * point.reference_coordinates[1], 1e-13 * c);
}
const auto& k = response.Value().high.tangent;
EXPECT_NEAR(k(0, 0), 41.0 / 18, 2e-14);
EXPECT_NEAR(k(0, 1), 19.0 / 30, 2e-14);
const auto nonlinear = brick.EvaluateNonlinearHighLow(high, low);
ASSERT_TRUE(nonlinear.HasValue());
const auto cubic_energy = Subtract({nonlinear.Value().high.strain_energy,
nonlinear.Value().strain_energy_low},
{response.Value().high.strain_energy,
response.Value().strain_energy_low});
EXPECT_NEAR(cubic_energy.high, (5 * 4.8 / 18 + 8.0 / 6) * c * c * c,
1e-12 * c * c * c);
}
} // namespace } // namespace