Integrate Refactor solver sources and dual MSVC CMake builds
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
#include "fesa/elements/element_capabilities.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/hex8_element.h"
|
||||
#include "fesa/elements/nonlinear_mitc4_inertia.h"
|
||||
#include "fesa/elements/thermal_shell.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void Same(double a, double b) {
|
||||
EXPECT_EQ(std::memcmp(&a, &b, sizeof(double)), 0);
|
||||
}
|
||||
template <std::size_t N>
|
||||
void Same(const std::array<double, N>& a, const std::array<double, N>& b) {
|
||||
for (std::size_t i = 0; i < N; ++i) Same(a[i], b[i]);
|
||||
}
|
||||
void Same(const fesa::Vector& a, const fesa::Vector& b) {
|
||||
ASSERT_EQ(a.Size(), b.Size());
|
||||
for (std::size_t i = 0; i < a.Size(); ++i) Same(a[i], b[i]);
|
||||
}
|
||||
void Same(const fesa::Matrix& a, const fesa::Matrix& b) {
|
||||
ASSERT_EQ(a.Rows(), b.Rows());
|
||||
ASSERT_EQ(a.Columns(), b.Columns());
|
||||
for (std::size_t i = 0; i < a.Rows(); ++i)
|
||||
for (std::size_t j = 0; j < a.Columns(); ++j) Same(a(i, j), b(i, j));
|
||||
}
|
||||
void Same(const fesa::Vector3& a, const fesa::Vector3& b) {
|
||||
for (std::size_t i = 0; i < 3; ++i) Same(a[i], b[i]);
|
||||
}
|
||||
void Same(const fesa::Status& a, const fesa::Status& b) {
|
||||
EXPECT_EQ(a.Category(), b.Category());
|
||||
ASSERT_EQ(a.Diagnostics().size(), b.Diagnostics().size());
|
||||
for (std::size_t i = 0; i < a.Diagnostics().size(); ++i) {
|
||||
const auto& x = a.Diagnostics()[i];
|
||||
const auto& y = b.Diagnostics()[i];
|
||||
EXPECT_EQ(x.code, y.code);
|
||||
EXPECT_EQ(x.location.file, y.location.file);
|
||||
EXPECT_EQ(x.location.line, y.location.line);
|
||||
EXPECT_EQ(x.keyword, y.keyword);
|
||||
EXPECT_EQ(x.entity_identity, y.entity_identity);
|
||||
EXPECT_EQ(x.message, y.message);
|
||||
}
|
||||
}
|
||||
fesa::ElementDofLayout ShellLayout() {
|
||||
return {{"shell", 9, "0009"}, {7, 2, 9, 4},
|
||||
{fesa::DofComponent::kUx, fesa::DofComponent::kUy,
|
||||
fesa::DofComponent::kUz, fesa::DofComponent::kUrx,
|
||||
fesa::DofComponent::kUry, fesa::DofComponent::kUrz}};
|
||||
}
|
||||
struct ShellInput {
|
||||
std::array<fesa::Node, 4> nodes{{
|
||||
{{"shell", 1, "1"}, {-1., -1., 0.}, {"cap.inp", 1}},
|
||||
{{"shell", 2, "2"}, {1., -1., 0.}, {"cap.inp", 2}},
|
||||
{{"shell", 3, "3"}, {1., 1., 0.}, {"cap.inp", 3}},
|
||||
{{"shell", 4, "4"}, {-1., 1., 0.}, {"cap.inp", 4}}}};
|
||||
std::array<fesa::ShellNodeInitialFrame, 4> frames{{
|
||||
{0, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}},
|
||||
{1, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}},
|
||||
{2, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}},
|
||||
{3, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}}}};
|
||||
fesa::ShellSection section{"section", .2, 0, {"cap.inp", 20}};
|
||||
fesa::LinearElasticMaterial material{"elastic", 120., .25, {"cap.inp", 21}};
|
||||
std::array<const fesa::Node*, 4> Pointers() const {
|
||||
return {&nodes[0], &nodes[1], &nodes[2], &nodes[3]};
|
||||
}
|
||||
};
|
||||
fesa::NonlinearMitc4TrialDofs Trial() {
|
||||
fesa::NonlinearMitc4TrialDofs trial{};
|
||||
for (std::size_t i = 0; i < 4; ++i) {
|
||||
const double n = static_cast<double>(i + 1);
|
||||
trial.displacement[i] = {.001 * n, -.002 * n, .0005 * n};
|
||||
trial.rotation_vector[i] = {.01 * n, -.005 * n, .002 * n};
|
||||
}
|
||||
return trial;
|
||||
}
|
||||
void Same(const fesa::NonlinearMitc4Recovery& a,
|
||||
const fesa::NonlinearMitc4Recovery& b) {
|
||||
Same(a.physical_strain_energy, b.physical_strain_energy);
|
||||
for (std::size_t g = 0; g < 4; ++g) {
|
||||
const auto& x = a.gauss_points[g];
|
||||
const auto& y = b.gauss_points[g];
|
||||
EXPECT_EQ(x.location, y.location);
|
||||
for (std::size_t i = 0; i < 2; ++i)
|
||||
Same(x.natural_coordinates[i], y.natural_coordinates[i]);
|
||||
for (std::size_t i = 0; i < 8; ++i) {
|
||||
Same(x.generalized_strain[i], y.generalized_strain[i]);
|
||||
Same(x.section_resultant[i], y.section_resultant[i]);
|
||||
}
|
||||
Same(x.local_frame.e1, y.local_frame.e1);
|
||||
Same(x.local_frame.e2, y.local_frame.e2);
|
||||
Same(x.local_frame.e3, y.local_frame.e3);
|
||||
for (std::size_t s = 0; s < 3; ++s)
|
||||
for (std::size_t i = 0; i < x.stress[s].components.size(); ++i)
|
||||
Same(x.stress[s].components[i], y.stress[s].components[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ElementCapabilities, ShellTrialPreservesCompleteDirectionalResponseAndFailure) {
|
||||
ShellInput input;
|
||||
auto made = fesa::NonlinearMitc4Shell::Create(
|
||||
input.Pointers(), input.frames, input.section, input.material);
|
||||
ASSERT_TRUE(made.HasValue());
|
||||
auto kernel = std::make_unique<fesa::NonlinearMitc4Shell>(std::move(made.Value()));
|
||||
std::unique_ptr<fesa::ShellTrialElement> capability =
|
||||
std::make_unique<fesa::Mitc4TrialAdapter>(ShellLayout(), *kernel);
|
||||
const auto trial = Trial();
|
||||
fesa::Vector direction{24};
|
||||
for (std::size_t i = 0; i < 24; ++i) direction[i] = .003 * (i + 1);
|
||||
const auto saved_direction = direction;
|
||||
const auto direct = fesa::EvaluateInternalResidualAndTangent(*kernel, trial, &direction);
|
||||
const auto via = capability->EvaluateTrial(trial, &direction);
|
||||
ASSERT_TRUE(direct.HasValue());
|
||||
ASSERT_TRUE(via.HasValue());
|
||||
const auto& a = direct.Value();
|
||||
const auto& b = via.Value();
|
||||
Same(a.internal_residual, b.internal_residual);
|
||||
Same(a.internal_tangent, b.internal_tangent);
|
||||
Same(a.internal_force, b.internal_force);
|
||||
Same(a.recovery, b.recovery);
|
||||
Same(a.physical_strain_energy, b.physical_strain_energy);
|
||||
Same(a.physical_force, b.physical_force);
|
||||
Same(a.drilling_force, b.drilling_force);
|
||||
Same(a.drilling_tangent, b.drilling_tangent);
|
||||
Same(a.drilling_metric, b.drilling_metric);
|
||||
Same(a.drilling_energy, b.drilling_energy);
|
||||
Same(a.force_directional_second, b.force_directional_second);
|
||||
Same(a.drilling_constraint, b.drilling_constraint);
|
||||
Same(a.drilling_constraint_gradient, b.drilling_constraint_gradient);
|
||||
for (std::size_t n = 0; n < 4; ++n) {
|
||||
Same(a.drilling_constraint_hessian[n], b.drilling_constraint_hessian[n]);
|
||||
Same(a.drilling_constraint_third_directional_gradient[n],
|
||||
b.drilling_constraint_third_directional_gradient[n]);
|
||||
}
|
||||
Same(direction, saved_direction);
|
||||
EXPECT_EQ(capability->DofLayout().node_indices, ShellLayout().node_indices);
|
||||
EXPECT_EQ(capability->DofLayout().components_per_node, ShellLayout().components_per_node);
|
||||
auto invalid = trial;
|
||||
invalid.rotation_vector[2] = {std::numeric_limits<double>::quiet_NaN(), 0., 0.};
|
||||
const auto failed_direct = fesa::EvaluateInternalResidualAndTangent(*kernel, invalid);
|
||||
const auto failed_via = capability->EvaluateTrial(invalid);
|
||||
ASSERT_FALSE(failed_direct.HasValue());
|
||||
ASSERT_FALSE(failed_via.HasValue());
|
||||
Same(failed_direct.GetStatus(), failed_via.GetStatus());
|
||||
const auto again = capability->EvaluateTrial(trial, &direction);
|
||||
ASSERT_TRUE(again.HasValue());
|
||||
Same(again.Value().internal_force, a.internal_force);
|
||||
}
|
||||
|
||||
TEST(ElementCapabilities, ShellExternalConstraintRetainsEveryVariation) {
|
||||
ShellInput input;
|
||||
const auto kernel = fesa::NonlinearMitc4Shell::Create(
|
||||
input.Pointers(), input.frames, input.section, input.material);
|
||||
ASSERT_TRUE(kernel.HasValue());
|
||||
fesa::Mitc4TrialAdapter adapter{ShellLayout(), kernel.Value()};
|
||||
const fesa::ShellTrialElement& capability = adapter;
|
||||
const fesa::Vector3 phi{.12, -.23, .07}, rate{.31, .02, -.17};
|
||||
const fesa::Vector3 force{1., -2., 3.}, force_rate{.2, .7, -.4}, acceleration{.1, -.3, .9};
|
||||
const auto direct = kernel.Value().EvaluateExternalDrillingConstraint(
|
||||
2, phi, rate, force, force_rate, acceleration);
|
||||
const auto via = capability.EvaluateExternalDrillingConstraint(
|
||||
2, phi, rate, force, force_rate, acceleration);
|
||||
ASSERT_TRUE(direct.HasValue());
|
||||
ASSERT_TRUE(via.HasValue());
|
||||
const auto& a = direct.Value();
|
||||
const auto& b = via.Value();
|
||||
Same(a.value, b.value);
|
||||
Same(a.gradient, b.gradient);
|
||||
Same(a.hessian, b.hessian);
|
||||
Same(a.directional_first, b.directional_first);
|
||||
Same(a.directional_second, b.directional_second);
|
||||
Same(a.gradient_directional_first, b.gradient_directional_first);
|
||||
Same(a.gradient_directional_second, b.gradient_directional_second);
|
||||
const auto failed_direct = kernel.Value().EvaluateExternalDrillingConstraint(
|
||||
4, phi, rate, force, force_rate, acceleration);
|
||||
const auto failed_via = capability.EvaluateExternalDrillingConstraint(
|
||||
4, phi, rate, force, force_rate, acceleration);
|
||||
ASSERT_FALSE(failed_direct.HasValue());
|
||||
ASSERT_FALSE(failed_via.HasValue());
|
||||
Same(failed_direct.GetStatus(), failed_via.GetStatus());
|
||||
}
|
||||
|
||||
TEST(ElementCapabilities, ShellInertiaRetainsCompletePhysicalResponse) {
|
||||
ShellInput input;
|
||||
const auto kernel = fesa::NonlinearMitc4Inertia::Create(
|
||||
input.Pointers(), input.frames, input.section, input.material, 2.);
|
||||
ASSERT_TRUE(kernel.HasValue());
|
||||
std::unique_ptr<fesa::ShellInertiaElement> capability =
|
||||
std::make_unique<fesa::Mitc4InertiaAdapter>(ShellLayout(), kernel.Value());
|
||||
const auto trial = Trial();
|
||||
fesa::Vector v{24}, acceleration{24};
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
v[i] = .013 * (i + 1);
|
||||
acceleration[i] = -.007 * (i + 1);
|
||||
}
|
||||
const auto saved_v = v, saved_acceleration = acceleration;
|
||||
const auto direct = kernel.Value().Evaluate(trial, v, acceleration);
|
||||
const auto via = capability->Evaluate(trial, v, acceleration);
|
||||
ASSERT_TRUE(direct.HasValue());
|
||||
ASSERT_TRUE(via.HasValue());
|
||||
const auto& a = direct.Value();
|
||||
const auto& b = via.Value();
|
||||
Same(a.mass, b.mass);
|
||||
Same(a.position_tangent, b.position_tangent);
|
||||
Same(a.velocity_tangent, b.velocity_tangent);
|
||||
Same(a.force, b.force);
|
||||
Same(a.convective_force, b.convective_force);
|
||||
Same(a.kinetic_energy, b.kinetic_energy);
|
||||
Same(a.momentum, b.momentum);
|
||||
Same(a.angular_momentum, b.angular_momentum);
|
||||
Same(a.momentum_rate, b.momentum_rate);
|
||||
Same(a.angular_momentum_rate, b.angular_momentum_rate);
|
||||
Same(a.momentum_rate_scale, b.momentum_rate_scale);
|
||||
Same(a.angular_momentum_rate_scale, b.angular_momentum_rate_scale);
|
||||
Same(v, saved_v);
|
||||
Same(acceleration, saved_acceleration);
|
||||
const auto failed_direct = kernel.Value().Evaluate(trial, fesa::Vector{23}, acceleration);
|
||||
const auto failed_via = capability->Evaluate(trial, fesa::Vector{23}, acceleration);
|
||||
ASSERT_FALSE(failed_direct.HasValue());
|
||||
ASSERT_FALSE(failed_via.HasValue());
|
||||
Same(failed_direct.GetStatus(), failed_via.GetStatus());
|
||||
}
|
||||
|
||||
TEST(ElementCapabilities, ThermalPreservesStationLayoutIntervalsAndAllTraces) {
|
||||
const std::array<fesa::Vector3, 4> x{{{0., 0., 0.}, {1., 0., 0.},
|
||||
{1., 1., 0.}, {0., 1., 0.}}};
|
||||
const auto kernel = fesa::ThermalShell::Create(
|
||||
{"thermal", 19, "0019"}, {7, 2, 9, 4}, x, 2., 3., 4., .5);
|
||||
ASSERT_TRUE(kernel.HasValue());
|
||||
std::unique_ptr<fesa::ThermalElement> capability =
|
||||
std::make_unique<fesa::Ds4ThermalAdapter>(kernel.Value());
|
||||
EXPECT_EQ(capability->DofLayout().source_id.source_label_text, "0019");
|
||||
EXPECT_EQ(capability->DofLayout().node_indices, kernel.Value().NodeIndices());
|
||||
for (std::size_t i = 0; i < 5; ++i)
|
||||
EXPECT_EQ(static_cast<int>(capability->DofLayout().components[i]), 11 + i);
|
||||
EXPECT_EQ(&capability->Conductivity(), &kernel.Value().Conductivity());
|
||||
EXPECT_EQ(&capability->Capacity(), &kernel.Value().Capacity());
|
||||
EXPECT_EQ(&capability->ConductivityError(), &kernel.Value().ConductivityError());
|
||||
EXPECT_EQ(&capability->CapacityError(), &kernel.Value().CapacityError());
|
||||
std::array<double, 20> temperature{};
|
||||
for (std::size_t i = 0; i < 20; ++i) temperature[i] = static_cast<double>(i * i);
|
||||
const auto saved = temperature;
|
||||
const auto direct = kernel.Value().Recover(temperature);
|
||||
const auto via = capability->Recover(temperature);
|
||||
ASSERT_TRUE(direct.HasValue());
|
||||
ASSERT_TRUE(via.HasValue());
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
Same(direct.Value().global_flux[i], via.Value().global_flux[i]);
|
||||
for (std::size_t p = 0; p < 4; ++p)
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
Same(direct.Value().frames[p][i], via.Value().frames[p][i]);
|
||||
EXPECT_EQ(temperature, saved);
|
||||
temperature[9] = std::numeric_limits<double>::infinity();
|
||||
const auto failed_direct = kernel.Value().Recover(temperature);
|
||||
const auto failed_via = capability->Recover(temperature);
|
||||
ASSERT_FALSE(failed_direct.HasValue());
|
||||
ASSERT_FALSE(failed_via.HasValue());
|
||||
Same(failed_direct.GetStatus(), failed_via.GetStatus());
|
||||
}
|
||||
|
||||
fesa::Result<fesa::Hex8Element> Solid() {
|
||||
const fesa::Hex8Element::Coordinates x{{
|
||||
{-1., -1., -1.}, {1., -1., -1.}, {1., 1., -1.}, {-1., 1., -1.},
|
||||
{-1., -1., 1.}, {1., -1., 1.}, {1., 1., 1.}, {-1., 1., 1.}}};
|
||||
return fesa::Hex8Element::Create(
|
||||
3, {{"solid", 42, "0042"}, {7, 2, 9, 4, 8, 3, 10, 5},
|
||||
{fesa::DofComponent::kUx, fesa::DofComponent::kUy,
|
||||
fesa::DofComponent::kUz}},
|
||||
x, fesa::LinearElasticMaterial{"elastic", 120., .25, {}}, 2.);
|
||||
}
|
||||
void Same(const fesa::Hex8Response& a, const fesa::Hex8Response& b) {
|
||||
Same(a.internal_force, b.internal_force);
|
||||
Same(a.material_tangent, b.material_tangent);
|
||||
Same(a.geometric_tangent, b.geometric_tangent);
|
||||
Same(a.tangent, b.tangent);
|
||||
Same(a.strain_energy, b.strain_energy);
|
||||
for (std::size_t i = 0; i < 8; ++i) {
|
||||
const auto& x = a.points[i];
|
||||
const auto& y = b.points[i];
|
||||
EXPECT_EQ(x.element, y.element);
|
||||
EXPECT_EQ(x.integration_point, y.integration_point);
|
||||
Same(x.natural_coordinates, y.natural_coordinates);
|
||||
Same(x.reference_coordinates, y.reference_coordinates);
|
||||
Same(x.strain, y.strain);
|
||||
Same(x.cauchy_stress, y.cauchy_stress);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ElementCapabilities, SolidRetainsLinearNonlinearAndPairedResponses) {
|
||||
auto made = Solid();
|
||||
ASSERT_TRUE(made.HasValue());
|
||||
const auto& kernel = made.Value();
|
||||
const fesa::Hex8TrialAdapter adapter{kernel};
|
||||
const fesa::SolidTrialElement& capability = adapter;
|
||||
const fesa::Element& base = capability;
|
||||
EXPECT_EQ(&base.DofLayout(), &kernel.DofLayout());
|
||||
fesa::Vector u{24}, low{24};
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
u[i] = .001 * static_cast<double>(i + 1);
|
||||
low[i] = 1e-20 * static_cast<double>(i + 1);
|
||||
}
|
||||
const auto saved_u = u, saved_low = low;
|
||||
const auto linear = kernel.EvaluateLinear(u);
|
||||
const auto via_linear = capability.EvaluateLinear(u);
|
||||
const auto nonlinear = kernel.EvaluateNonlinear(u);
|
||||
const auto via_nonlinear = capability.EvaluateNonlinear(u);
|
||||
ASSERT_TRUE(linear.HasValue());
|
||||
ASSERT_TRUE(via_linear.HasValue());
|
||||
ASSERT_TRUE(nonlinear.HasValue());
|
||||
ASSERT_TRUE(via_nonlinear.HasValue());
|
||||
Same(linear.Value(), via_linear.Value());
|
||||
Same(nonlinear.Value(), via_nonlinear.Value());
|
||||
for (const bool nonlinear_path : {false, true}) {
|
||||
const auto direct = nonlinear_path ? kernel.EvaluateNonlinearHighLow(u, low)
|
||||
: kernel.EvaluateLinearHighLow(u, low);
|
||||
const auto via = nonlinear_path ? capability.EvaluateNonlinearHighLow(u, low)
|
||||
: capability.EvaluateLinearHighLow(u, low);
|
||||
ASSERT_TRUE(direct.HasValue());
|
||||
ASSERT_TRUE(via.HasValue());
|
||||
Same(direct.Value().high, via.Value().high);
|
||||
Same(direct.Value().internal_force_low, via.Value().internal_force_low);
|
||||
Same(direct.Value().tangent_low, via.Value().tangent_low);
|
||||
Same(direct.Value().strain_energy_low, via.Value().strain_energy_low);
|
||||
}
|
||||
const auto k = kernel.ComputeStiffness(), via_k = base.ComputeStiffness();
|
||||
const auto m = kernel.ComputeMass(), via_m = capability.ComputeMass();
|
||||
ASSERT_TRUE(k.HasValue());
|
||||
ASSERT_TRUE(via_k.HasValue());
|
||||
ASSERT_TRUE(m.HasValue());
|
||||
ASSERT_TRUE(via_m.HasValue());
|
||||
Same(k.Value().values, via_k.Value().values);
|
||||
Same(m.Value().values, via_m.Value().values);
|
||||
Same(kernel.ReferenceWeights(), capability.ReferenceWeights());
|
||||
const auto recovered = base.Recover(u), direct_recovered = kernel.Recover(u);
|
||||
ASSERT_TRUE(recovered.HasValue());
|
||||
ASSERT_TRUE(direct_recovered.HasValue());
|
||||
EXPECT_EQ(recovered.Value().source_id.source_label_text, "0042");
|
||||
const auto& rows = std::get<fesa::SolidElementResultRows>(recovered.Value().payload);
|
||||
const auto& direct_rows = std::get<fesa::SolidElementResultRows>(direct_recovered.Value().payload);
|
||||
ASSERT_EQ(rows.rows.size(), direct_rows.rows.size());
|
||||
for (std::size_t i = 0; i < rows.rows.size(); ++i) {
|
||||
EXPECT_EQ(rows.rows[i].element, direct_rows.rows[i].element);
|
||||
EXPECT_EQ(rows.rows[i].integration_point, direct_rows.rows[i].integration_point);
|
||||
Same(rows.rows[i].cauchy_stress, direct_rows.rows[i].cauchy_stress);
|
||||
Same(rows.rows[i].strain, direct_rows.rows[i].strain);
|
||||
}
|
||||
Same(u, saved_u);
|
||||
Same(low, saved_low);
|
||||
const auto failed = capability.EvaluateNonlinear(fesa::Vector{23});
|
||||
const auto direct_failed = kernel.EvaluateNonlinear(fesa::Vector{23});
|
||||
ASSERT_FALSE(failed.HasValue());
|
||||
ASSERT_FALSE(direct_failed.HasValue());
|
||||
Same(failed.GetStatus(), direct_failed.GetStatus());
|
||||
const auto paired_failed = capability.EvaluateNonlinearHighLow(u, fesa::Vector{23});
|
||||
const auto direct_paired_failed = kernel.EvaluateNonlinearHighLow(u, fesa::Vector{23});
|
||||
ASSERT_FALSE(paired_failed.HasValue());
|
||||
ASSERT_FALSE(direct_paired_failed.HasValue());
|
||||
Same(paired_failed.GetStatus(), direct_paired_failed.GetStatus());
|
||||
const auto again = capability.EvaluateNonlinear(u);
|
||||
ASSERT_TRUE(again.HasValue());
|
||||
Same(again.Value(), nonlinear.Value());
|
||||
}
|
||||
|
||||
TEST(ElementCapabilities, BoundViewsSurviveOwnerVectorMovesAndRejectOtherFamilies) {
|
||||
static_assert(std::has_virtual_destructor_v<fesa::ShellTrialElement>);
|
||||
static_assert(std::has_virtual_destructor_v<fesa::SolidTrialElement>);
|
||||
static_assert(std::has_virtual_destructor_v<fesa::ShellInertiaElement>);
|
||||
static_assert(std::has_virtual_destructor_v<fesa::ThermalElement>);
|
||||
static_assert(std::has_virtual_destructor_v<fesa::ShellReferenceElement>);
|
||||
auto made = Solid();
|
||||
ASSERT_TRUE(made.HasValue());
|
||||
std::vector<std::unique_ptr<fesa::Element>> owners;
|
||||
owners.push_back(std::make_unique<fesa::Hex8Element>(std::move(made.Value())));
|
||||
const auto* original = owners[0].get();
|
||||
const fesa::ElementFactory factory;
|
||||
auto capability = factory.BindSolidTrial(*original);
|
||||
ASSERT_NE(capability, nullptr);
|
||||
EXPECT_EQ(factory.BindShellReference(*original), nullptr);
|
||||
// Reallocation moves unique_ptr owners, never the kernels they own.
|
||||
owners.reserve(32);
|
||||
auto transferred = std::move(owners);
|
||||
EXPECT_EQ(transferred[0].get(), original);
|
||||
EXPECT_EQ(&capability->DofLayout(), &original->DofLayout());
|
||||
const auto evaluated = capability->EvaluateLinear(fesa::Vector{24});
|
||||
ASSERT_TRUE(evaluated.HasValue());
|
||||
capability.reset(); // Destruction through the role must not delete its kernel.
|
||||
const auto recovered = original->Recover(fesa::Vector{24});
|
||||
ASSERT_TRUE(recovered.HasValue());
|
||||
ShellInput input;
|
||||
auto shell = fesa::Mitc4Shell::Create(
|
||||
input.Pointers(), {{{0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}}},
|
||||
input.section, input.material);
|
||||
ASSERT_TRUE(shell.HasValue());
|
||||
EXPECT_EQ(factory.BindSolidTrial(shell.Value()), nullptr);
|
||||
auto reference = factory.BindShellReference(shell.Value());
|
||||
ASSERT_NE(reference, nullptr);
|
||||
EXPECT_EQ(&reference->DofLayout(), &shell.Value().DofLayout());
|
||||
const auto direct_k = shell.Value().Stiffness();
|
||||
const auto via_k = reference->Stiffness();
|
||||
ASSERT_TRUE(direct_k.HasValue());
|
||||
ASSERT_TRUE(via_k.HasValue());
|
||||
Same(direct_k.Value().stabilized_global24, via_k.Value().stabilized_global24);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -161,6 +161,28 @@ TEST(FiniteRotation, RejectsCoordinatesInsidePrincipalBranchMargin) {
|
||||
"invalid-accepted-state");
|
||||
}
|
||||
|
||||
TEST(FiniteRotation, SpatialMomentSolvePreservesKnownLoadAndVirtualWork) {
|
||||
const Vector3 moment{2.0, -3.0, 4.0};
|
||||
const Vector3 variation{0.3, -0.5, 0.7};
|
||||
const double pi = std::acos(-1.0);
|
||||
for (const double angle : {0.0, 1e-9, 0.7, pi - 1e-6}) {
|
||||
const double a = angle == 0.0 ? 1.0 : std::sin(angle) / angle;
|
||||
const double b = angle < 1e-8 ? angle / 2.0 :
|
||||
(1.0 - std::cos(angle)) / angle;
|
||||
// Independent z-axis J^T*m; the axial component is unchanged.
|
||||
const Vector3 generalized{a * moment.X() + b * moment.Y(),
|
||||
-b * moment.X() + a * moment.Y(), moment.Z()};
|
||||
const auto solved = SpatialMomentSO3(Vector3{0, 0, angle}, generalized);
|
||||
ASSERT_TRUE(solved.HasValue());
|
||||
ExpectVectorNear(solved.Value(), moment, 1e-11);
|
||||
const Vector3 spatial_variation{a * variation.X() - b * variation.Y(),
|
||||
b * variation.X() + a * variation.Y(),
|
||||
variation.Z()};
|
||||
EXPECT_NEAR(generalized.Dot(variation),
|
||||
solved.Value().Dot(spatial_variation), 1e-11);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FiniteRotation,
|
||||
JacobiansAreFiniteInverseTransposeAndDerivativeConsistent) {
|
||||
const double pi = std::acos(-1.0);
|
||||
@@ -172,18 +194,13 @@ TEST(FiniteRotation,
|
||||
|
||||
for (const Vector3& phi : rotations) {
|
||||
const Matrix left_jacobian = LeftJacobianSO3(phi);
|
||||
const Matrix inverse_transpose = InverseTransposeLeftJacobianSO3(phi);
|
||||
EXPECT_TRUE(IsFinite(left_jacobian));
|
||||
EXPECT_TRUE(IsFinite(inverse_transpose));
|
||||
|
||||
const Matrix identity =
|
||||
Transpose(left_jacobian).Multiply(inverse_transpose);
|
||||
for (std::size_t row = 0U; row < kRotationDimension; ++row) {
|
||||
for (std::size_t column = 0U; column < kRotationDimension; ++column) {
|
||||
EXPECT_NEAR(identity(row, column), row == column ? 1.0 : 0.0,
|
||||
1.0e-11);
|
||||
}
|
||||
}
|
||||
const Vector3 load{0.7, -0.2, 0.4};
|
||||
const auto spatial = SpatialMomentSO3(phi, load);
|
||||
ASSERT_TRUE(spatial.HasValue());
|
||||
ExpectVectorNear(Multiply(Transpose(left_jacobian), spatial.Value()),
|
||||
load, 1e-11);
|
||||
|
||||
const Vector3 director = Multiply(ExpSkew(phi), Vector3{0.0, 0.0, 1.0});
|
||||
const Vector3 positive =
|
||||
|
||||
@@ -811,36 +811,6 @@ TEST(Hex8Element, FiniteInputsThatOverflowDerivedArithmeticFailClosed) {
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST(Hex8Element, BackendOperandRejectionRetainsOriginalStiffnessDiagnostic) {
|
||||
Coordinates coordinates{};
|
||||
for (std::size_t a = 0; a < 8; ++a)
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
coordinates[a][i] = 0.01 * kSigns[a][i];
|
||||
// Original native probe: finite moduli/volume, but mu*gradient overflows.
|
||||
// The failed diagnostic candidate must not become a scalar success fallback.
|
||||
const auto element = Make(coordinates, 1e308, -0.5);
|
||||
ASSERT_FALSE(element.HasValue());
|
||||
EXPECT_EQ(element.GetStatus().Diagnostics()[0].code, "invalid-hex8");
|
||||
EXPECT_EQ(element.GetStatus().Diagnostics()[0].message,
|
||||
"Hex8 reference stiffness is nonfinite.");
|
||||
}
|
||||
|
||||
TEST(Hex8Element, TensorFailureRetainsPriorityOverEnergyAndTangentOverflow) {
|
||||
Coordinates coordinates{};
|
||||
for (std::size_t a = 0; a < 8; ++a)
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
coordinates[a][i] = 0.01 * kSigns[a][i];
|
||||
const auto element = Make(coordinates, 1e200, -0.5);
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
const auto displacement = Affine(coordinates,
|
||||
Tensor{{{1e100, 0, 0}, {0, 1e100, 0}, {0, 0, 1e100}}});
|
||||
const auto response = element.Value().EvaluateNonlinear(displacement);
|
||||
ASSERT_FALSE(response.HasValue());
|
||||
EXPECT_EQ(response.GetStatus().Diagnostics()[0].code, "invalid-hex8");
|
||||
EXPECT_EQ(response.GetStatus().Diagnostics()[0].message,
|
||||
"Hex8 trial tensor is nonfinite.");
|
||||
}
|
||||
|
||||
// Independent Gauss-Legendre abscissae from polynomial root iteration. This
|
||||
// test does not call the kernel's shapes, Jacobian, inverse or quadrature.
|
||||
std::vector<std::array<double, 2>> LegendreRule(unsigned count) {
|
||||
@@ -1211,3 +1181,17 @@ TEST(Hex8Element, MeanVolumeWarpedLowOnlyStateRetainsWeightedCubicEnergy) {
|
||||
EXPECT_NE(actual_cubic.high, 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(Hex8Element, RejectsNonfiniteCanonicalJacobianActionBeforeFiniteGradients) {
|
||||
auto coordinates = Brick();
|
||||
for (std::size_t a = 0; a < 8U; ++a) {
|
||||
coordinates[a][0] = kSigns[a][0] * std::ldexp(1.0, -1025);
|
||||
coordinates[a][1] = static_cast<double>(kSigns[a][1]);
|
||||
coordinates[a][2] = static_cast<double>(kSigns[a][2]);
|
||||
}
|
||||
const auto rejected = Make(coordinates);
|
||||
ASSERT_FALSE(rejected.HasValue());
|
||||
ASSERT_EQ(rejected.GetStatus().Diagnostics().size(), 1U);
|
||||
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].message,
|
||||
"Reference Jacobian inverse is nonfinite.");
|
||||
}
|
||||
|
||||
@@ -2,839 +2,16 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cfenv>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/materials/isotropic_linear_elastic_material.h"
|
||||
#include "../../../src/fesa/math/high_low_arithmetic.h"
|
||||
#include "../../../src/fesa/elements/hex8_geometry.h"
|
||||
#include "../../../src/fesa/elements/hex8_pair_arithmetic.h"
|
||||
#include "../../../src/fesa/elements/hex8_pair_simd.h"
|
||||
#include "../math/high_low_arithmetic_original.h"
|
||||
|
||||
namespace {
|
||||
using fesa::math_internal::HighLow;
|
||||
using fesa::math_internal::Subtract;
|
||||
|
||||
std::uint64_t PairBits(double value) {
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
return bits;
|
||||
}
|
||||
|
||||
void ExpectOriginalProduct(HighLow left, HighLow right) {
|
||||
const auto actual = fesa::hex8_internal::MultiplyPair(left, right);
|
||||
const auto expected = fesa::high_low_original::Multiply(
|
||||
{left.high, left.low}, {right.high, right.low});
|
||||
EXPECT_EQ(PairBits(actual.high), PairBits(expected.high));
|
||||
EXPECT_EQ(PairBits(actual.low), PairBits(expected.low));
|
||||
const auto actual_sum = fesa::hex8_internal::AddPair(left, right);
|
||||
const auto expected_sum = fesa::high_low_original::Add(
|
||||
{left.high, left.low}, {right.high, right.low});
|
||||
EXPECT_EQ(PairBits(actual_sum.high), PairBits(expected_sum.high));
|
||||
EXPECT_EQ(PairBits(actual_sum.low), PairBits(expected_sum.low));
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, ScalarCrossProductShortcutMatchesOriginalBits) {
|
||||
ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported());
|
||||
const double minimum = std::numeric_limits<double>::denorm_min();
|
||||
const double maximum = std::numeric_limits<double>::max();
|
||||
const double infinity = std::numeric_limits<double>::infinity();
|
||||
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||
std::vector<HighLow> pairs{{}, {-0.0, 0.0}, {0.0, -0.0}, {-0.0, -0.0},
|
||||
{1, -0.0}, {-1, -0.0}, {1, 1}, {0, minimum}, {infinity, 0},
|
||||
{0, infinity}, {nan, 0}, {0, nan}, {maximum}, {-maximum},
|
||||
{minimum}, {-minimum}, {std::numeric_limits<double>::min()},
|
||||
{0.5}, {std::nextafter(0.5, 1.0)},
|
||||
{1, std::ldexp(1.0, -53)},
|
||||
{std::nextafter(1.0, 2.0), -std::ldexp(1.0, -53)}};
|
||||
for (const auto left : pairs)
|
||||
for (const auto right : pairs) ExpectOriginalProduct(left, right);
|
||||
for (int exponent = -1074; exponent <= 1023; ++exponent) {
|
||||
const double value = std::ldexp(1.0, exponent);
|
||||
for (double sign : {-1.0, 1.0})
|
||||
for (const auto right : pairs) {
|
||||
ExpectOriginalProduct(HighLow{sign * value}, right);
|
||||
ExpectOriginalProduct(right, HighLow{sign * value});
|
||||
}
|
||||
}
|
||||
std::mt19937_64 random(0x48e81234U);
|
||||
for (int sample = 0; sample < 12000; ++sample) {
|
||||
double left = 0;
|
||||
double right = 0;
|
||||
const auto left_bits = random();
|
||||
const auto right_bits = random();
|
||||
std::memcpy(&left, &left_bits, sizeof(left));
|
||||
std::memcpy(&right, &right_bits, sizeof(right));
|
||||
const auto pair = fesa::math_internal::TwoSum(left, right);
|
||||
ExpectOriginalProduct(pair, HighLow{});
|
||||
ExpectOriginalProduct(HighLow{}, pair);
|
||||
ExpectOriginalProduct(pair, HighLow{right});
|
||||
ExpectOriginalProduct(HighLow{right}, pair);
|
||||
ExpectOriginalProduct(pair, pair);
|
||||
}
|
||||
}
|
||||
|
||||
fesa::hex8_internal::PacketPair RawPacketPair(HighLow value) {
|
||||
return {value.high, value.low};
|
||||
}
|
||||
|
||||
fesa::hex8_internal::PairPacket FrozenMaterialPacket(
|
||||
const fesa::hex8_internal::MaterialPacketInput& input) {
|
||||
using Original = fesa::high_low_original::HighLow;
|
||||
const auto original = [](fesa::hex8_internal::PacketPair value) {
|
||||
return Original{value.high, value.low};
|
||||
};
|
||||
fesa::hex8_internal::PairPacket result;
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
const auto first = fesa::high_low_original::Multiply(
|
||||
original(input.deviatoric_left.values[lane]),
|
||||
original(input.deviatoric_right.values[lane]));
|
||||
const auto second = fesa::high_low_original::Multiply(
|
||||
original(input.shear_left.values[lane]), original(input.shear_right.values[lane]));
|
||||
const auto third = fesa::high_low_original::Multiply(
|
||||
original(input.tensor_left.values[lane]), original(input.gradient_dot.values[lane]));
|
||||
const auto value = fesa::high_low_original::Add(
|
||||
fesa::high_low_original::Add(first, second), third);
|
||||
const auto accumulated = fesa::high_low_original::Add(
|
||||
original(input.accumulated.values[lane]),
|
||||
fesa::high_low_original::Multiply(value, original(input.weight)));
|
||||
result.values[lane] = {accumulated.high, accumulated.low};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ExpectFrozenMaterialPacket(
|
||||
const fesa::hex8_internal::MaterialPacketInput& input) {
|
||||
const auto expected = FrozenMaterialPacket(input);
|
||||
fesa::hex8_internal::PairPacket scalar, selected;
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar(input, &scalar);
|
||||
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
|
||||
evaluator(input, &selected);
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
SCOPED_TRACE(lane);
|
||||
EXPECT_EQ(PairBits(scalar.values[lane].high), PairBits(expected.values[lane].high));
|
||||
EXPECT_EQ(PairBits(scalar.values[lane].low), PairBits(expected.values[lane].low));
|
||||
EXPECT_EQ(PairBits(selected.values[lane].high), PairBits(expected.values[lane].high));
|
||||
EXPECT_EQ(PairBits(selected.values[lane].low), PairBits(expected.values[lane].low));
|
||||
}
|
||||
return evaluator != fesa::hex8_internal::EvaluateMaterialPacketScalar;
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, FourColumnMaterialPacketMatchesFrozenScalarBits) {
|
||||
ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported());
|
||||
using fesa::hex8_internal::MaterialPacketInput;
|
||||
RecordProperty("private_packet_backend",
|
||||
fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar ? "scalar" : "avx2");
|
||||
std::size_t packet_checks = 0, avx2_evaluator_checks = 0;
|
||||
const double tiny = std::numeric_limits<double>::denorm_min();
|
||||
const double maximum = std::numeric_limits<double>::max();
|
||||
const std::vector<HighLow> boundary{{}, {-0.0, 0}, {0, -0.0},
|
||||
{1}, {-1}, {1, -0.0}, {1, std::ldexp(1.0, -54)},
|
||||
{-1, std::ldexp(1.0, -54)}, {tiny}, {-tiny},
|
||||
{std::numeric_limits<double>::min()}, {maximum}, {-maximum},
|
||||
{1, tiny}, {1, -tiny}, {std::nextafter(1.0, 2.0)},
|
||||
{1, 1}, {0, tiny}, {std::numeric_limits<double>::infinity(), 0},
|
||||
{std::numeric_limits<double>::quiet_NaN(), 0},
|
||||
{std::numeric_limits<double>::signaling_NaN(), 0},
|
||||
{0, std::numeric_limits<double>::signaling_NaN()}};
|
||||
for (std::size_t first = 0; first < boundary.size(); ++first)
|
||||
for (std::size_t second = 0; second < boundary.size(); ++second) {
|
||||
MaterialPacketInput input{};
|
||||
input.weight = {0.125};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
input.deviatoric_left.values[lane] = RawPacketPair(boundary[first]);
|
||||
input.deviatoric_right.values[lane] = RawPacketPair(boundary[second]);
|
||||
input.shear_left.values[lane] = {2, std::ldexp(1.0, -54)};
|
||||
input.shear_right.values[lane] = {-0.5, std::ldexp(1.0, -56)};
|
||||
input.tensor_left.values[lane] = {0.75};
|
||||
input.gradient_dot.values[lane] = {-0.25};
|
||||
input.accumulated.values[lane] = lane == 0 ? fesa::hex8_internal::PacketPair{0, 0}
|
||||
: fesa::hex8_internal::PacketPair{0.125, 0};
|
||||
}
|
||||
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
|
||||
++packet_checks;
|
||||
}
|
||||
for (int exponent = -1074; exponent <= 1023; ++exponent) {
|
||||
MaterialPacketInput input{};
|
||||
input.weight = {1};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
input.deviatoric_left.values[lane] = {
|
||||
std::ldexp(lane % 2U == 0 ? 1.0 : -1.0, exponent)};
|
||||
input.deviatoric_right.values[lane] = {lane < 2 ? 0.5 : 2.0};
|
||||
}
|
||||
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
|
||||
++packet_checks;
|
||||
}
|
||||
std::mt19937_64 random(0x4c0116U);
|
||||
for (std::size_t sample = 0; sample < 2048; ++sample) {
|
||||
MaterialPacketInput input{};
|
||||
const auto pair = [&]() {
|
||||
const double high = std::ldexp(
|
||||
1.0 + static_cast<double>(random() % 256U) / 256.0,
|
||||
static_cast<int>(random() % 201U) - 100);
|
||||
const double signed_high = (random() & 1U) != 0 ? high : -high;
|
||||
const double low = sample % 3U == 0 ? 0.0 : std::ldexp(high, -55);
|
||||
return fesa::hex8_internal::PacketPair{
|
||||
signed_high, (random() & 1U) != 0 ? low : -low};
|
||||
};
|
||||
input.weight = sample % 2U == 0
|
||||
? fesa::hex8_internal::PacketPair{0.125, 0} : pair();
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
input.deviatoric_left.values[lane] = pair();
|
||||
input.deviatoric_right.values[lane] = pair();
|
||||
input.shear_left.values[lane] = pair();
|
||||
input.shear_right.values[lane] = pair();
|
||||
input.tensor_left.values[lane] = pair();
|
||||
input.gradient_dot.values[lane] = pair();
|
||||
input.accumulated.values[lane] = sample % 4U == 0
|
||||
? fesa::hex8_internal::PacketPair{0, 0} : pair();
|
||||
}
|
||||
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
|
||||
++packet_checks;
|
||||
}
|
||||
RecordProperty("packet_oracle_checks", static_cast<int>(packet_checks));
|
||||
RecordProperty("avx2_evaluator_packet_checks",
|
||||
static_cast<int>(avx2_evaluator_checks));
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, PacketSpecialLaneDoesNotChangeOtherColumns) {
|
||||
fesa::hex8_internal::MaterialPacketInput input{};
|
||||
input.weight = {0.125};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
input.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)};
|
||||
input.deviatoric_right.values[lane] = {2, std::ldexp(1.0, -55)};
|
||||
input.shear_left.values[lane] = {0.5};
|
||||
input.shear_right.values[lane] = {-0.25};
|
||||
input.tensor_left.values[lane] = {0.75};
|
||||
input.gradient_dot.values[lane] = {0.125};
|
||||
}
|
||||
const auto reference = FrozenMaterialPacket(input);
|
||||
const std::vector<HighLow> special{{}, {-0.0, 0}, {1},
|
||||
{std::numeric_limits<double>::max()},
|
||||
{std::numeric_limits<double>::denorm_min()}, {1, 1},
|
||||
{std::numeric_limits<double>::quiet_NaN(), 0}};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane)
|
||||
for (const auto value : special) {
|
||||
auto changed = input;
|
||||
changed.deviatoric_left.values[lane] = RawPacketPair(value);
|
||||
ExpectFrozenMaterialPacket(changed);
|
||||
fesa::hex8_internal::PairPacket actual;
|
||||
fesa::hex8_internal::SelectMaterialPacketEvaluator()(changed, &actual);
|
||||
for (std::size_t other = 0; other < 4; ++other)
|
||||
if (other != lane) {
|
||||
EXPECT_EQ(PairBits(actual.values[other].high), PairBits(reference.values[other].high));
|
||||
EXPECT_EQ(PairBits(actual.values[other].low), PairBits(reference.values[other].low));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, UniformDyadicPacketsExerciseEachLowZeroShape) {
|
||||
std::size_t checks = 0, avx2_checks = 0;
|
||||
for (int shape = 0; shape < 4; ++shape)
|
||||
for (int low_weight = 0; low_weight < 2; ++low_weight)
|
||||
for (int zero_accumulator = 0; zero_accumulator < 2; ++zero_accumulator) {
|
||||
fesa::hex8_internal::MaterialPacketInput input{};
|
||||
input.weight = {0.5, low_weight != 0 ? std::ldexp(1.0, -57) : 0.0};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
const bool left_low = (shape & 1) != 0;
|
||||
const bool right_low = (shape & 2) != 0;
|
||||
// Every lane has the same normalized nonzero dyadic inputs. Component
|
||||
// LBE sums stay above -200 and all Add intermediates are bounded by 4.
|
||||
// Thus each Multiply low-shape is uniform and its component products
|
||||
// bypass zero/unsupported-tail fallback. No mixed-zero Add fallback
|
||||
// is possible; the initial zero accumulator is uniform across lanes.
|
||||
input.deviatoric_left.values[lane] = {1, left_low ? std::ldexp(1.0, -54) : 0};
|
||||
input.deviatoric_right.values[lane] = {2, right_low ? std::ldexp(1.0, -54) : 0};
|
||||
input.shear_left.values[lane] = {0.5, left_low ? std::ldexp(1.0, -56) : 0};
|
||||
input.shear_right.values[lane] = {0.25, right_low ? std::ldexp(1.0, -57) : 0};
|
||||
input.tensor_left.values[lane] = {0.75, left_low ? std::ldexp(1.0, -56) : 0};
|
||||
input.gradient_dot.values[lane] = {0.125, right_low ? std::ldexp(1.0, -58) : 0};
|
||||
input.accumulated.values[lane] = zero_accumulator != 0
|
||||
? fesa::hex8_internal::PacketPair{0, 0}
|
||||
: fesa::hex8_internal::PacketPair{0.0625, 0};
|
||||
}
|
||||
avx2_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
|
||||
++checks;
|
||||
}
|
||||
RecordProperty("uniform_low_shape_checks", static_cast<int>(checks));
|
||||
RecordProperty("avx2_uniform_low_shape_checks", static_cast<int>(avx2_checks));
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, OverflowLaneIsNeutralizedBeforeResidualFma) {
|
||||
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
|
||||
if (evaluator == fesa::hex8_internal::EvaluateMaterialPacketScalar)
|
||||
GTEST_SKIP() << "AVX2 masked environment is not selected; routing unverified.";
|
||||
std::fenv_t original;
|
||||
ASSERT_EQ(std::fegetenv(&original), 0);
|
||||
for (std::size_t bad = 0; bad < 4; ++bad) {
|
||||
fesa::hex8_internal::MaterialPacketInput input{};
|
||||
input.weight = {0.5};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
input.deviatoric_left.values[lane] = {
|
||||
lane == bad ? std::numeric_limits<double>::max() : 1};
|
||||
input.deviatoric_right.values[lane] = {2};
|
||||
input.shear_left.values[lane] = {0.5};
|
||||
input.shear_right.values[lane] = {0.25};
|
||||
input.tensor_left.values[lane] = {0.75};
|
||||
input.gradient_dot.values[lane] = {0.125};
|
||||
input.accumulated.values[lane] = {0.0625};
|
||||
}
|
||||
// All operands are finite/nonzero and max*2 has LBE sum 972, so Product
|
||||
// reaches its vector multiply; only the post-product finite mask rejects it.
|
||||
const auto expected = FrozenMaterialPacket(input);
|
||||
EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0);
|
||||
fesa::hex8_internal::PairPacket output;
|
||||
evaluator(input, &output);
|
||||
const bool extra_invalid = (std::fetestexcept(FE_INVALID) & FE_INVALID) != 0;
|
||||
EXPECT_EQ(std::fesetenv(&original), 0);
|
||||
EXPECT_FALSE(extra_invalid);
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
EXPECT_EQ(PairBits(output.values[lane].high), PairBits(expected.values[lane].high));
|
||||
EXPECT_EQ(PairBits(output.values[lane].low), PairBits(expected.values[lane].low));
|
||||
}
|
||||
}
|
||||
RecordProperty("avx2_product_overflow_positions", 4);
|
||||
}
|
||||
|
||||
#if defined(_M_X64) || defined(__SSE2__)
|
||||
// Runs scalar and selected packet from identical FP state. The frozen original
|
||||
// supplies raw-bit results; the unchanged baseline supplies FE/MXCSR status.
|
||||
void ExpectPacketFlagsAndBits(
|
||||
const fesa::hex8_internal::MaterialPacketInput& input, bool prior_status) {
|
||||
std::fenv_t original;
|
||||
ASSERT_EQ(std::fegetenv(&original), 0);
|
||||
const auto original_csr = _mm_getcsr();
|
||||
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
|
||||
struct Observation {
|
||||
fesa::hex8_internal::PairPacket output;
|
||||
int flags;
|
||||
unsigned int status;
|
||||
};
|
||||
const auto observe = [&](fesa::hex8_internal::MaterialPacketEvaluator backend) {
|
||||
EXPECT_EQ(std::fesetenv(&original), 0);
|
||||
_mm_setcsr(original_csr & ~0x3fU);
|
||||
EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0);
|
||||
if (prior_status) {
|
||||
EXPECT_EQ(std::feraiseexcept(FE_INEXACT), 0);
|
||||
_mm_setcsr(_mm_getcsr() | 0x02U); // Preserve prior denormal status too.
|
||||
}
|
||||
Observation value{};
|
||||
backend(input, &value.output);
|
||||
value.flags = std::fetestexcept(FE_ALL_EXCEPT);
|
||||
value.status = _mm_getcsr() & 0x3fU;
|
||||
return value;
|
||||
};
|
||||
const auto scalar = observe(fesa::hex8_internal::EvaluateMaterialPacketScalar);
|
||||
const auto selected = observe(evaluator);
|
||||
const auto expected = FrozenMaterialPacket(input);
|
||||
// Restore both x87 and MXCSR before assertions, including NaN bit assertions.
|
||||
EXPECT_EQ(std::fesetenv(&original), 0);
|
||||
_mm_setcsr(original_csr);
|
||||
EXPECT_EQ(selected.flags, scalar.flags);
|
||||
EXPECT_EQ(selected.status, scalar.status);
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
SCOPED_TRACE(lane);
|
||||
EXPECT_EQ(PairBits(selected.output.values[lane].high),
|
||||
PairBits(expected.values[lane].high));
|
||||
EXPECT_EQ(PairBits(selected.output.values[lane].low),
|
||||
PairBits(expected.values[lane].low));
|
||||
EXPECT_EQ(PairBits(scalar.output.values[lane].high),
|
||||
PairBits(expected.values[lane].high));
|
||||
EXPECT_EQ(PairBits(scalar.output.values[lane].low),
|
||||
PairBits(expected.values[lane].low));
|
||||
}
|
||||
}
|
||||
|
||||
fesa::hex8_internal::MaterialPacketInput DyadicPacketFixture() {
|
||||
fesa::hex8_internal::MaterialPacketInput input{};
|
||||
input.weight = {0.5, 0};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
input.deviatoric_left.values[lane] = {1, 0};
|
||||
input.deviatoric_right.values[lane] = {2, 0};
|
||||
input.shear_left.values[lane] = {0.5, 0};
|
||||
input.shear_right.values[lane] = {0.25, 0};
|
||||
input.tensor_left.values[lane] = {0.75, 0};
|
||||
input.gradient_dot.values[lane] = {0.125, 0};
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, PacketVectorGuardsPreserveScalarFlagsAndBits) {
|
||||
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar)
|
||||
GTEST_SKIP() << "AVX2 masked environment is not selected.";
|
||||
using fesa::hex8_internal::PacketPair;
|
||||
const double maximum = std::numeric_limits<double>::max();
|
||||
const double snan = std::numeric_limits<double>::signaling_NaN();
|
||||
const double tiny = std::numeric_limits<double>::denorm_min();
|
||||
const std::vector<PacketPair> invalid_left{
|
||||
{-0.0, 0}, {0, -0.0}, {1, 1}, {snan, 0}, {0, snan}};
|
||||
const std::vector<PacketPair> dangerous_right{
|
||||
{maximum, maximum}, {snan, 0}, {0, snan}};
|
||||
std::size_t checks = 0;
|
||||
for (const auto left : invalid_left)
|
||||
for (const auto right : dangerous_right)
|
||||
for (std::size_t bad = 0; bad < 4; ++bad) {
|
||||
auto input = DyadicPacketFixture();
|
||||
input.deviatoric_left.values[bad] = left;
|
||||
input.deviatoric_right.values[bad] = right;
|
||||
// Later lanes must still contribute the unchanged bridge's RN status.
|
||||
const auto later = (bad + 1) % 4;
|
||||
input.deviatoric_left.values[later] = {1, std::ldexp(1.0, -54)};
|
||||
input.deviatoric_right.values[later] = {tiny, 0};
|
||||
ExpectPacketFlagsAndBits(input, false);
|
||||
ExpectPacketFlagsAndBits(input, true);
|
||||
checks += 2;
|
||||
}
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
auto mixed = DyadicPacketFixture();
|
||||
mixed.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)};
|
||||
mixed.deviatoric_left.values[(lane + 1) % 4] = {maximum, maximum};
|
||||
ExpectPacketFlagsAndBits(mixed, false);
|
||||
ExpectPacketFlagsAndBits(mixed, true);
|
||||
auto signed_low = DyadicPacketFixture();
|
||||
signed_low.deviatoric_left.values[lane].low = -0.0;
|
||||
signed_low.accumulated.values[lane] = {-0.0, 0};
|
||||
ExpectPacketFlagsAndBits(signed_low, false);
|
||||
checks += 3;
|
||||
}
|
||||
RecordProperty("avx2_guard_flags_and_bit_checks", static_cast<int>(checks));
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, PacketTailScreenRetainsExactBoundaryAndFallback) {
|
||||
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar)
|
||||
GTEST_SKIP() << "AVX2 masked environment is not selected.";
|
||||
const auto odd = [](int exponent) {
|
||||
return std::nextafter(std::ldexp(1.0, exponent),
|
||||
std::numeric_limits<double>::infinity());
|
||||
};
|
||||
const double tiny = std::numeric_limits<double>::denorm_min();
|
||||
const std::array<std::array<double, 2>, 8> operands{{
|
||||
{odd(-500), odd(-470)}, // screen true, exact LBE sum -1074
|
||||
{odd(-500), odd(-471)}, // screen false, exact rejection -1075
|
||||
{std::ldexp(1.0, -500), std::ldexp(1.0, -471)}, // false but supported
|
||||
{tiny, odd(52)}, {tiny, odd(51)}, {3 * tiny, odd(52)},
|
||||
{std::numeric_limits<double>::max(), 2}, {0, odd(52)}}};
|
||||
std::size_t checks = 0;
|
||||
for (const auto& pair : operands)
|
||||
for (std::size_t special = 0; special < 4; ++special) {
|
||||
fesa::hex8_internal::MaterialPacketInput input{};
|
||||
input.weight = {1, 0};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
const double sign = lane % 2 == 0 ? 1.0 : -1.0;
|
||||
input.deviatoric_left.values[lane] =
|
||||
{sign * (lane == special ? pair[0] : odd(-500)), 0};
|
||||
input.deviatoric_right.values[lane] =
|
||||
{lane == special ? pair[1] : odd(-470), 0};
|
||||
}
|
||||
ExpectPacketFlagsAndBits(input, false);
|
||||
ExpectPacketFlagsAndBits(input, true);
|
||||
checks += 2;
|
||||
}
|
||||
RecordProperty("avx2_tail_screen_flags_and_bit_checks", static_cast<int>(checks));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(_M_X64) || defined(__SSE2__)
|
||||
fesa::hex8_internal::GeometryPacketOutput FrozenGeometryPacket(
|
||||
const fesa::hex8_internal::GeometryPacketInput& input) {
|
||||
const auto pair = [](fesa::hex8_internal::PacketPair value) {
|
||||
return fesa::high_low_original::HighLow{value.high, value.low};
|
||||
};
|
||||
fesa::hex8_internal::GeometryPacketOutput output{};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
fesa::high_low_original::HighLow dot;
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
dot = fesa::high_low_original::Add(dot,
|
||||
fesa::high_low_original::Multiply(pair(input.gradient_a[i]),
|
||||
pair(input.gradient_b[i].values[lane])));
|
||||
fesa::high_low_original::HighLow geo;
|
||||
for (std::size_t j = 0; j < 3; ++j)
|
||||
for (std::size_t l = 0; l < 3; ++l)
|
||||
geo = fesa::high_low_original::Add(geo,
|
||||
fesa::high_low_original::Multiply(pair(input.gradient_stress[j][l]),
|
||||
pair(input.gradient_b[l].values[lane])));
|
||||
const auto weighted = fesa::high_low_original::Multiply(geo, pair(input.weight));
|
||||
output.gradient_dot.values[lane] = {dot.high, dot.low};
|
||||
output.weighted_geometric.values[lane] = {weighted.high, weighted.low};
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fesa::hex8_internal::GeometryPacketInput GeometryDyadicFixture() {
|
||||
fesa::hex8_internal::GeometryPacketInput input{};
|
||||
input.weight = {0.125, 0};
|
||||
for (std::size_t j = 0; j < 3; ++j) {
|
||||
input.gradient_a[j] = {static_cast<double>(j + 1) * 0.25, 0};
|
||||
for (std::size_t l = 0; l < 3; ++l) {
|
||||
// Deliberately asymmetric first-products and distinct b/component mapping.
|
||||
input.gradient_stress[j][l] =
|
||||
{static_cast<double>(1 + 3 * j + l) * 0.125, 0};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane)
|
||||
input.gradient_b[l].values[lane] =
|
||||
{static_cast<double>(1 + 4 * l + lane) * 0.0625, 0};
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
// Original observation repeats geo*weight for three independent accumulators.
|
||||
// Selected observation reuses the weighted pair but retains each checked Add.
|
||||
void ExpectGeometryPacketBitsAndFlags(
|
||||
const fesa::hex8_internal::GeometryPacketInput& input, bool prior_status) {
|
||||
SCOPED_TRACE(prior_status);
|
||||
std::fenv_t original;
|
||||
ASSERT_EQ(std::fegetenv(&original), 0);
|
||||
const auto original_csr = _mm_getcsr();
|
||||
struct Observation {
|
||||
fesa::hex8_internal::GeometryPacketOutput output;
|
||||
fesa::hex8_internal::PairPacket accumulators[3];
|
||||
int flags;
|
||||
unsigned int status;
|
||||
};
|
||||
const auto current_pair = [](fesa::hex8_internal::PacketPair value) {
|
||||
return HighLow{value.high, value.low};
|
||||
};
|
||||
const auto observe = [&](bool original_order) {
|
||||
EXPECT_EQ(std::fesetenv(&original), 0);
|
||||
_mm_setcsr(original_csr & ~0x3fU);
|
||||
EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0);
|
||||
if (prior_status) {
|
||||
EXPECT_EQ(std::feraiseexcept(FE_INEXACT), 0);
|
||||
_mm_setcsr(_mm_getcsr() | 0x02U);
|
||||
}
|
||||
Observation value{};
|
||||
if (!original_order)
|
||||
fesa::hex8_internal::EvaluateGeometryPacketAvx2(input, &value.output);
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
HighLow geo;
|
||||
if (original_order) {
|
||||
HighLow dot;
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
dot = fesa::hex8_internal::AddPair(dot,
|
||||
fesa::hex8_internal::MultiplyPair(current_pair(input.gradient_a[i]),
|
||||
current_pair(input.gradient_b[i].values[lane])));
|
||||
for (std::size_t j = 0; j < 3; ++j)
|
||||
for (std::size_t l = 0; l < 3; ++l)
|
||||
geo = fesa::hex8_internal::AddPair(geo,
|
||||
fesa::hex8_internal::MultiplyPair(
|
||||
current_pair(input.gradient_stress[j][l]),
|
||||
current_pair(input.gradient_b[l].values[lane])));
|
||||
value.output.gradient_dot.values[lane] = {dot.high, dot.low};
|
||||
}
|
||||
const HighLow accumulators[3]{{},
|
||||
{static_cast<double>(lane + 1), std::ldexp(1.0, -54)}, {-1, -0.0}};
|
||||
for (std::size_t component = 0; component < 3; ++component) {
|
||||
const auto weighted = original_order
|
||||
? fesa::hex8_internal::MultiplyPair(geo, current_pair(input.weight))
|
||||
: current_pair(value.output.weighted_geometric.values[lane]);
|
||||
if (original_order)
|
||||
value.output.weighted_geometric.values[lane] =
|
||||
{weighted.high, weighted.low};
|
||||
const auto accumulated = fesa::hex8_internal::AddPair(
|
||||
accumulators[component], weighted);
|
||||
value.accumulators[component].values[lane] =
|
||||
{accumulated.high, accumulated.low};
|
||||
}
|
||||
}
|
||||
value.flags = std::fetestexcept(FE_ALL_EXCEPT);
|
||||
value.status = _mm_getcsr() & 0x3fU;
|
||||
return value;
|
||||
};
|
||||
const auto baseline = observe(true);
|
||||
const auto selected = observe(false);
|
||||
fesa::hex8_internal::GeometryPacketOutput scalar;
|
||||
fesa::hex8_internal::EvaluateGeometryPacketScalar(input, &scalar);
|
||||
const auto expected = FrozenGeometryPacket(input);
|
||||
EXPECT_EQ(std::fesetenv(&original), 0);
|
||||
_mm_setcsr(original_csr);
|
||||
EXPECT_EQ(selected.flags, baseline.flags);
|
||||
EXPECT_EQ(selected.status, baseline.status);
|
||||
const auto same_bits = [](const fesa::hex8_internal::PairPacket& a,
|
||||
const fesa::hex8_internal::PairPacket& b) {
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
EXPECT_EQ(PairBits(a.values[lane].high), PairBits(b.values[lane].high));
|
||||
EXPECT_EQ(PairBits(a.values[lane].low), PairBits(b.values[lane].low));
|
||||
}
|
||||
};
|
||||
same_bits(selected.output.gradient_dot, expected.gradient_dot);
|
||||
same_bits(selected.output.weighted_geometric, expected.weighted_geometric);
|
||||
same_bits(scalar.gradient_dot, expected.gradient_dot);
|
||||
same_bits(scalar.weighted_geometric, expected.weighted_geometric);
|
||||
for (std::size_t component = 0; component < 3; ++component)
|
||||
same_bits(selected.accumulators[component], baseline.accumulators[component]);
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, GeometryPacketPreservesAsymmetricLanesAndUniformShapes) {
|
||||
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar)
|
||||
GTEST_SKIP() << "Geometry AVX2 masked environment is not selected.";
|
||||
RecordProperty("private_geometry_packet_backend", "avx2");
|
||||
static_assert(std::is_trivial_v<fesa::hex8_internal::GeometryPacketInput>);
|
||||
static_assert(std::is_trivial_v<fesa::hex8_internal::GeometryPacketOutput>);
|
||||
std::size_t checks = 0;
|
||||
for (int shape = 0; shape < 4; ++shape) {
|
||||
auto input = GeometryDyadicFixture();
|
||||
for (std::size_t j = 0; j < 3; ++j) {
|
||||
if ((shape & 1) != 0) {
|
||||
input.gradient_a[j].low = std::ldexp(input.gradient_a[j].high, -55);
|
||||
for (std::size_t l = 0; l < 3; ++l)
|
||||
input.gradient_stress[j][l].low =
|
||||
std::ldexp(input.gradient_stress[j][l].high, -55);
|
||||
}
|
||||
if ((shape & 2) != 0)
|
||||
for (std::size_t lane = 0; lane < 4; ++lane)
|
||||
input.gradient_b[j].values[lane].low =
|
||||
std::ldexp(input.gradient_b[j].values[lane].high, -55);
|
||||
}
|
||||
ExpectGeometryPacketBitsAndFlags(input, false);
|
||||
ExpectGeometryPacketBitsAndFlags(input, true);
|
||||
checks += 2;
|
||||
}
|
||||
RecordProperty("avx2_geometry_uniform_bit_flag_checks", static_cast<int>(checks));
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, GeometryPacketRetainsSpecialLanesAndExactTailBoundary) {
|
||||
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar)
|
||||
GTEST_SKIP() << "Geometry AVX2 masked environment is not selected.";
|
||||
using fesa::hex8_internal::PacketPair;
|
||||
const double tiny = std::numeric_limits<double>::denorm_min();
|
||||
const double maximum = std::numeric_limits<double>::max();
|
||||
const std::vector<PacketPair> special{{0, 0}, {-0.0, 0}, {1, -0.0}, {1, 1},
|
||||
{tiny, 0}, {maximum, maximum},
|
||||
{std::numeric_limits<double>::signaling_NaN(), 0},
|
||||
{0, std::numeric_limits<double>::signaling_NaN()}};
|
||||
std::size_t checks = 0;
|
||||
for (const auto pair : special)
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
auto input = GeometryDyadicFixture();
|
||||
input.gradient_b[1].values[lane] = pair;
|
||||
ExpectGeometryPacketBitsAndFlags(input, false);
|
||||
ExpectGeometryPacketBitsAndFlags(input, true);
|
||||
checks += 2;
|
||||
}
|
||||
const auto odd = [](int exponent) {
|
||||
return std::nextafter(std::ldexp(1.0, exponent),
|
||||
std::numeric_limits<double>::infinity());
|
||||
};
|
||||
for (int exponent : {-470, -471})
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
auto input = GeometryDyadicFixture();
|
||||
input.gradient_stress[0][0] = {odd(-500), 0};
|
||||
input.gradient_b[0].values[lane] = {odd(exponent), 0};
|
||||
ExpectGeometryPacketBitsAndFlags(input, false);
|
||||
checks++;
|
||||
}
|
||||
RecordProperty("avx2_geometry_special_tail_checks", static_cast<int>(checks));
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, GeometryPacketPreservesOrderedOverflowAndPreparedInvalid) {
|
||||
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar)
|
||||
GTEST_SKIP() << "Geometry AVX2 masked environment is not selected.";
|
||||
const double maximum = std::numeric_limits<double>::max();
|
||||
std::size_t checks = 0;
|
||||
for (bool overflow_order : {false, true}) {
|
||||
SCOPED_TRACE(overflow_order);
|
||||
fesa::hex8_internal::GeometryPacketInput input{};
|
||||
input.weight = {0.5, 0};
|
||||
for (std::size_t l = 0; l < 3; ++l)
|
||||
for (std::size_t lane = 0; lane < 4; ++lane)
|
||||
input.gradient_b[l].values[lane] = {1, 0};
|
||||
input.gradient_stress[0][0] = {maximum, 0};
|
||||
input.gradient_stress[0][1] = {overflow_order ? maximum : -maximum, 0};
|
||||
input.gradient_stress[0][2] = {overflow_order ? -maximum : maximum, 0};
|
||||
ExpectGeometryPacketBitsAndFlags(input, false);
|
||||
ExpectGeometryPacketBitsAndFlags(input, true);
|
||||
checks += 2;
|
||||
}
|
||||
auto input = GeometryDyadicFixture();
|
||||
const auto invalid_first = fesa::high_low_original::Multiply(
|
||||
{std::ldexp(1.0, -600), 0}, {std::ldexp(1.0, -500), 0});
|
||||
input.gradient_stress[0][0] = {invalid_first.high, invalid_first.low};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane)
|
||||
input.gradient_b[0].values[lane] = {std::ldexp(1.0, 600), 0};
|
||||
ExpectGeometryPacketBitsAndFlags(input, false);
|
||||
ExpectGeometryPacketBitsAndFlags(input, true);
|
||||
checks += 2;
|
||||
RecordProperty("avx2_geometry_ordered_invalid_checks", static_cast<int>(checks));
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, GeometryPacketSumFallbackPreservesNearMaxAndSubnormalStatus) {
|
||||
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar)
|
||||
GTEST_SKIP() << "Geometry AVX2 masked environment is not selected.";
|
||||
const double maximum = std::numeric_limits<double>::max();
|
||||
const double normal = std::ldexp(1.0, -1022);
|
||||
const double tiny = std::ldexp(1.0, -1074);
|
||||
const double quarter = std::ldexp(1.0, 969);
|
||||
std::size_t checks = 0;
|
||||
for (double tail : {0.0, normal, tiny})
|
||||
for (std::size_t bad = 0; bad < 4; ++bad) {
|
||||
SCOPED_TRACE(tail);
|
||||
SCOPED_TRACE(bad);
|
||||
fesa::hex8_internal::GeometryPacketInput input{};
|
||||
input.weight = {0.5, 0};
|
||||
// Both pairs are normalized. Head sum is finite; middle contributes
|
||||
// 2^970 and the joined sum overflows. Other lanes retain finite values.
|
||||
input.gradient_stress[0][0] = {maximum, quarter};
|
||||
input.gradient_stress[0][1] = {quarter, tail};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
const double scale = lane == bad ? 1.0 : 0.5;
|
||||
input.gradient_b[0].values[lane] = {scale, 0};
|
||||
input.gradient_b[1].values[lane] = {scale, 0};
|
||||
input.gradient_b[2].values[lane] = {1, 0};
|
||||
}
|
||||
ExpectGeometryPacketBitsAndFlags(input, false);
|
||||
ExpectGeometryPacketBitsAndFlags(input, true);
|
||||
checks += 2;
|
||||
}
|
||||
fesa::hex8_internal::GeometryPacketInput finite{};
|
||||
finite.weight = {1, 0};
|
||||
finite.gradient_stress[0][0] = {normal, 0};
|
||||
finite.gradient_stress[0][1] = {normal + tiny, 0};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
finite.gradient_b[0].values[lane] = {1, 0};
|
||||
finite.gradient_b[1].values[lane] = {1, 0};
|
||||
}
|
||||
// TwoSum(N,N+d) has finite normal inputs and a subnormal error d.
|
||||
ExpectGeometryPacketBitsAndFlags(finite, false);
|
||||
ExpectGeometryPacketBitsAndFlags(finite, true);
|
||||
checks += 2;
|
||||
// Mixed prefix: finite subnormal-error lanes run before another lane's head
|
||||
// overflow. The bridge must preserve the whole packet's status union.
|
||||
for (std::size_t bad = 0; bad < 4; ++bad) {
|
||||
SCOPED_TRACE(bad);
|
||||
fesa::hex8_internal::GeometryPacketInput mixed{};
|
||||
mixed.weight = {1, 0};
|
||||
mixed.gradient_stress[0][2] = {normal, 0};
|
||||
mixed.gradient_stress[1][2] = {normal + tiny, 0};
|
||||
mixed.gradient_stress[2][0] = {maximum, 0};
|
||||
mixed.gradient_stress[2][1] = {maximum, 0};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
mixed.gradient_b[0].values[lane] = {lane == bad ? 1.0 : 0.0, 0};
|
||||
mixed.gradient_b[1].values[lane] = {lane == bad ? 1.0 : 0.0, 0};
|
||||
mixed.gradient_b[2].values[lane] = {lane == bad ? 0.0 : 1.0, 0};
|
||||
}
|
||||
ExpectGeometryPacketBitsAndFlags(mixed, false);
|
||||
ExpectGeometryPacketBitsAndFlags(mixed, true);
|
||||
checks += 2;
|
||||
}
|
||||
RecordProperty("avx2_geometry_sum_fallback_checks", static_cast<int>(checks));
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(Hex8HighLow, PacketBackendSelectionRecoversAfterEnvironmentRestoration) {
|
||||
const auto initial = fesa::hex8_internal::SelectMaterialPacketEvaluator();
|
||||
const int rounding = std::fegetround();
|
||||
ASSERT_EQ(std::fesetround(FE_UPWARD), 0);
|
||||
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(),
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar);
|
||||
EXPECT_EQ(std::fesetround(rounding), 0);
|
||||
#if defined(_M_X64) || defined(__SSE2__)
|
||||
const auto control = _mm_getcsr();
|
||||
_mm_setcsr(control | 0x8040U);
|
||||
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(),
|
||||
fesa::hex8_internal::EvaluateMaterialPacketScalar);
|
||||
_mm_setcsr(control);
|
||||
_mm_setcsr(control & ~0x80U);
|
||||
const auto unmasked = fesa::hex8_internal::SelectMaterialPacketEvaluator();
|
||||
_mm_setcsr(control);
|
||||
EXPECT_EQ(unmasked, fesa::hex8_internal::EvaluateMaterialPacketScalar);
|
||||
#endif
|
||||
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(), initial);
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, PacketScalarBridgesPreserveOriginalRawBits) {
|
||||
using fesa::hex8_internal::PacketPair;
|
||||
using fesa::hex8_internal::PairPacket;
|
||||
static_assert(std::is_trivial_v<PacketPair> &&
|
||||
std::is_standard_layout_v<PacketPair>);
|
||||
static_assert(std::is_trivial_v<PairPacket> &&
|
||||
std::is_standard_layout_v<PairPacket>);
|
||||
static_assert(std::is_trivial_v<fesa::hex8_internal::MaterialPacketInput>);
|
||||
const double tiny = std::numeric_limits<double>::denorm_min();
|
||||
const std::vector<PacketPair> pairs{{0, 0}, {-0.0, 0}, {0, -0.0},
|
||||
{1, -0.0}, {1, std::ldexp(1.0, -54)}, {tiny, 0}, {1, tiny},
|
||||
{std::numeric_limits<double>::max(), 0}, {1, 1},
|
||||
{std::numeric_limits<double>::infinity(), 0},
|
||||
{std::numeric_limits<double>::signaling_NaN(), 0}};
|
||||
for (const auto left : pairs)
|
||||
for (const auto right : pairs) {
|
||||
PairPacket a{{left, left, left, left}};
|
||||
PairPacket b{{right, right, right, right}};
|
||||
PairPacket result;
|
||||
const fesa::high_low_original::HighLow x{left.high, left.low};
|
||||
const fesa::high_low_original::HighLow y{right.high, right.low};
|
||||
const auto expect = [&](fesa::high_low_original::HighLow expected) {
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
EXPECT_EQ(PairBits(result.values[lane].high), PairBits(expected.high));
|
||||
EXPECT_EQ(PairBits(result.values[lane].low), PairBits(expected.low));
|
||||
}
|
||||
};
|
||||
fesa::hex8_internal::ScalarPacketAdd(a, b, false, &result);
|
||||
expect(fesa::high_low_original::Add(x, y));
|
||||
fesa::hex8_internal::ScalarPacketAdd(a, b, true, &result);
|
||||
expect(fesa::high_low_original::Add(x, y));
|
||||
fesa::hex8_internal::ScalarPacketMultiply(a, b, &result);
|
||||
expect(fesa::high_low_original::Multiply(x, y));
|
||||
const double high_left[4]{left.high, left.high, left.high, left.high};
|
||||
const double high_right[4]{right.high, right.high, right.high, right.high};
|
||||
fesa::hex8_internal::ScalarPacketTwoProduct(high_left, high_right, &result);
|
||||
expect(fesa::high_low_original::TwoProduct(left.high, right.high));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, PacketCapabilityRejectsMissingIntegerIsaAndOsState) {
|
||||
constexpr std::uint32_t leaf1 =
|
||||
(1U << 12) | (1U << 26) | (1U << 27) | (1U << 28);
|
||||
constexpr std::uint32_t leaf7 = (1U << 3) | (1U << 5) | (1U << 8);
|
||||
EXPECT_TRUE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
|
||||
leaf1, 6, leaf7));
|
||||
for (const auto missing : {1U << 3, 1U << 5, 1U << 8})
|
||||
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
|
||||
leaf1, 6, leaf7 & ~missing));
|
||||
for (const auto missing : {1U << 12, 1U << 26, 1U << 27, 1U << 28})
|
||||
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
|
||||
leaf1 & ~missing, 6, leaf7));
|
||||
for (std::uint64_t state : {0ULL, 2ULL, 4ULL})
|
||||
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
|
||||
leaf1, state, leaf7));
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, PacketDoesNotClearPriorMaskedStickyFlags) {
|
||||
std::fenv_t original;
|
||||
ASSERT_EQ(std::fegetenv(&original), 0);
|
||||
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
|
||||
ASSERT_EQ(std::feraiseexcept(FE_INEXACT), 0);
|
||||
fesa::hex8_internal::MaterialPacketInput input{};
|
||||
input.weight = {1};
|
||||
for (std::size_t lane = 0; lane < 4; ++lane) {
|
||||
input.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)};
|
||||
input.deviatoric_right.values[lane] = {2, std::ldexp(1.0, -54)};
|
||||
}
|
||||
fesa::hex8_internal::PairPacket output;
|
||||
evaluator(input, &output);
|
||||
const bool preserved = (std::fetestexcept(FE_INEXACT) & FE_INEXACT) != 0;
|
||||
EXPECT_EQ(std::fesetenv(&original), 0);
|
||||
EXPECT_TRUE(preserved);
|
||||
}
|
||||
|
||||
fesa::Hex8Element UnitBrick(double offset = 0, double nu = 0) {
|
||||
fesa::ElementDofLayout layout;
|
||||
layout.node_indices = {0, 1, 2, 3, 4, 5, 6, 7};
|
||||
@@ -850,116 +27,6 @@ fesa::Hex8Element UnitBrick(double offset = 0, double nu = 0) {
|
||||
return fesa::Hex8Element::Create(0, layout, coordinates, material, 2).Value();
|
||||
}
|
||||
|
||||
void ExpectSamePairResponse(const fesa::Hex8HighLowResponse& actual,
|
||||
const fesa::Hex8HighLowResponse& expected) {
|
||||
EXPECT_EQ(PairBits(actual.high.strain_energy),
|
||||
PairBits(expected.high.strain_energy));
|
||||
EXPECT_EQ(PairBits(actual.strain_energy_low),
|
||||
PairBits(expected.strain_energy_low));
|
||||
ASSERT_EQ(actual.high.tangent.Rows(), 24U);
|
||||
ASSERT_EQ(actual.tangent_low.Rows(), 24U);
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
EXPECT_EQ(PairBits(actual.high.internal_force[i]),
|
||||
PairBits(expected.high.internal_force[i]));
|
||||
EXPECT_EQ(PairBits(actual.internal_force_low[i]),
|
||||
PairBits(expected.internal_force_low[i]));
|
||||
for (std::size_t j = 0; j < 24; ++j) {
|
||||
EXPECT_EQ(PairBits(actual.high.tangent(i, j)),
|
||||
PairBits(expected.high.tangent(i, j)));
|
||||
EXPECT_EQ(PairBits(actual.tangent_low(i, j)),
|
||||
PairBits(expected.tangent_low(i, j)));
|
||||
}
|
||||
}
|
||||
for (std::size_t point = 0; point < 8; ++point) {
|
||||
EXPECT_EQ(actual.high.points[point].integration_point,
|
||||
expected.high.points[point].integration_point);
|
||||
for (std::size_t i = 0; i < 6; ++i) {
|
||||
EXPECT_EQ(PairBits(actual.high.points[point].cauchy_stress[i]),
|
||||
PairBits(expected.high.points[point].cauchy_stress[i]));
|
||||
EXPECT_EQ(PairBits(actual.high.points[point].strain[i]),
|
||||
PairBits(expected.high.points[point].strain[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, ConcurrentFirstLinearTrialsAcrossCopiesAndMoveKeepBits) {
|
||||
const auto element = UnitBrick();
|
||||
auto copied = element;
|
||||
const auto moved = std::move(copied);
|
||||
fesa::Vector high(24), low_a(24), low_b(24);
|
||||
for (std::size_t i = 0; i < 24; ++i) high[i] = 1;
|
||||
for (auto a : {1, 2, 5, 6}) {
|
||||
low_a[3U * a] = std::ldexp(1.0, -60);
|
||||
low_b[3U * a] = -std::ldexp(1.0, -59);
|
||||
}
|
||||
const auto oracle = UnitBrick();
|
||||
const auto expected_a = oracle.EvaluateLinearHighLow(high, low_a);
|
||||
const auto expected_b = oracle.EvaluateLinearHighLow(high, low_b);
|
||||
ASSERT_TRUE(expected_a.HasValue());
|
||||
ASSERT_TRUE(expected_b.HasValue());
|
||||
std::promise<void> start;
|
||||
const auto signal = start.get_future().share();
|
||||
std::atomic<std::size_t> arrived{0U};
|
||||
std::array<std::future<fesa::Result<fesa::Hex8HighLowResponse>>, 4> calls;
|
||||
for (std::size_t i = 0; i < calls.size(); ++i)
|
||||
calls[i] = std::async(std::launch::async, [&, i] {
|
||||
arrived.fetch_add(1U, std::memory_order_release);
|
||||
signal.wait();
|
||||
return (i % 2U == 0U ? element : moved)
|
||||
.EvaluateLinearHighLow(high, i < 2U ? low_a : low_b);
|
||||
});
|
||||
while (arrived.load(std::memory_order_acquire) != calls.size())
|
||||
std::this_thread::yield();
|
||||
start.set_value();
|
||||
for (std::size_t i = 0; i < calls.size(); ++i) {
|
||||
const auto response = calls[i].get();
|
||||
ASSERT_TRUE(response.HasValue());
|
||||
ExpectSamePairResponse(response.Value(),
|
||||
i < 2U ? expected_a.Value() : expected_b.Value());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, FirstLinearPreparationRecoversAfterTrialAndEnvironmentFailure) {
|
||||
ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported());
|
||||
const auto element = UnitBrick();
|
||||
fesa::Vector high(24), low(24);
|
||||
EXPECT_FALSE(element.EvaluateLinearHighLow(fesa::Vector(23), low).HasValue());
|
||||
high[0] = std::numeric_limits<double>::infinity();
|
||||
EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue());
|
||||
high[0] = 0;
|
||||
struct RestoreEnvironment {
|
||||
int rounding = std::fegetround();
|
||||
#if defined(_M_X64) || defined(__SSE2__)
|
||||
unsigned csr = _mm_getcsr();
|
||||
#endif
|
||||
~RestoreEnvironment() {
|
||||
std::fesetround(rounding);
|
||||
#if defined(_M_X64) || defined(__SSE2__)
|
||||
_mm_setcsr(csr);
|
||||
#endif
|
||||
}
|
||||
} restore;
|
||||
ASSERT_EQ(std::fesetround(FE_DOWNWARD), 0);
|
||||
const auto bad_environment = element.EvaluateLinearHighLow(high, low);
|
||||
ASSERT_FALSE(bad_environment.HasValue());
|
||||
EXPECT_EQ(bad_environment.GetStatus().Diagnostics()[0].message,
|
||||
"Hex8 high+low requires nearest rounding and gradual underflow.");
|
||||
ASSERT_EQ(std::fesetround(FE_TONEAREST), 0);
|
||||
#if defined(_M_X64) || defined(__SSE2__)
|
||||
_mm_setcsr(restore.csr | 0x8040U);
|
||||
EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue());
|
||||
_mm_setcsr(restore.csr);
|
||||
#endif
|
||||
const auto recovered = element.EvaluateLinearHighLow(high, low);
|
||||
ASSERT_TRUE(recovered.HasValue());
|
||||
high[3] = -2;
|
||||
EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue());
|
||||
high[3] = 0;
|
||||
const auto after_failure = element.EvaluateLinearHighLow(high, low);
|
||||
ASSERT_TRUE(after_failure.HasValue());
|
||||
ExpectSamePairResponse(after_failure.Value(), recovered.Value());
|
||||
}
|
||||
|
||||
TEST(Hex8HighLow, LowOnlyAffineStrainSurvivesUniformHighTranslation) {
|
||||
auto brick = UnitBrick();
|
||||
fesa::Vector high(24), low(24);
|
||||
|
||||
@@ -1173,3 +1173,16 @@ TEST(Mitc4ShellPhysicalRecovery,
|
||||
}
|
||||
EXPECT_NEAR(recovery.strain_energy, 72.16, 1.0e-12);
|
||||
}
|
||||
|
||||
TEST(Mitc4ShellKinematics, RetainsReciprocalDeterminantOverflowGateAtTinyScale) {
|
||||
const std::array<fesa::Node, 4> nodes{
|
||||
Node(0, {0.0, 0.0, 0.0}), Node(1, {2e-104, 0.0, 0.0}),
|
||||
Node(2, {2e-104, 2e-104, 0.0}), Node(3, {0.0, 2e-104, 0.0})};
|
||||
EXPECT_TRUE(fesa::Mitc4Shell::Create(
|
||||
NodePointers(nodes), Directors(), Section(2e-99), Material()).HasValue());
|
||||
const auto rejected = fesa::Mitc4Shell::Create(
|
||||
NodePointers(nodes), Directors(), Section(2e-102), Material());
|
||||
ASSERT_FALSE(rejected.HasValue());
|
||||
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].code,
|
||||
"invalid-shell-jacobian");
|
||||
}
|
||||
|
||||
@@ -494,25 +494,17 @@ TEST(DynamicRotation, InverseIdentityAndCurrentAnalyticNullAtFiniteRotation) {
|
||||
for (const V3 phi : {V3{0.5, 0.0, 0.0}, V3{0.2, -0.3, 0.1}}) {
|
||||
const auto rotation = fesa::ExpSkew(phi);
|
||||
const auto jacobian = fesa::LeftJacobianSO3(phi);
|
||||
const auto inverse_transpose = fesa::InverseTransposeLeftJacobianSO3(phi);
|
||||
for (std::size_t column = 0; column < 3; ++column) {
|
||||
// R^T e_j is a unit initial director; its current director is e_j.
|
||||
const V3 initial{rotation(column, 0), rotation(column, 1),
|
||||
rotation(column, 2)};
|
||||
const auto value = fesa::EvaluateDynamicRotation(phi, initial, {}, {});
|
||||
ASSERT_TRUE(value.HasValue());
|
||||
const V3 inverse_column{inverse_transpose(column, 0),
|
||||
inverse_transpose(column, 1),
|
||||
inverse_transpose(column, 2)};
|
||||
EXPECT_LE(
|
||||
(value.Value().drill_null - inverse_column / inverse_column.Norm())
|
||||
.Norm(),
|
||||
1.0e-12);
|
||||
const V3 identity_column =
|
||||
Multiply(jacobian, value.Value().drill_null) * inverse_column.Norm();
|
||||
for (std::size_t row = 0; row < 3; ++row) {
|
||||
EXPECT_NEAR(identity_column[row], row == column ? 1.0 : 0.0, 1.0e-12);
|
||||
}
|
||||
const V3 mapped = Multiply(jacobian, value.Value().drill_null);
|
||||
EXPECT_GT(mapped[column], 0.0);
|
||||
EXPECT_NEAR(value.Value().drill_null.Norm(), 1.0, 1e-12);
|
||||
for (std::size_t row = 0; row < 3; ++row)
|
||||
if (row != column) EXPECT_NEAR(mapped[row], 0.0, 1e-12);
|
||||
}
|
||||
}
|
||||
for (const Fixture fixture : {Fixture{}, Warped()}) {
|
||||
|
||||
Reference in New Issue
Block a user