feat: add eigen2 frequency pipeline and fail-closed validation

Implement MITC4 mass, constrained whitening, MKL FEAST, modal HDF5, and deterministic Value comparison. MSVC Debug builds; 394 of 395 tests pass. Original-model all-c residual and the documented S3 static contract conflict remain unresolved; this is an implementation checkpoint, not feature acceptance.
This commit is contained in:
KOKO\Mimi
2026-09-09 03:14:30 +09:00
parent e74ac247db
commit 22c9d4435c
39 changed files with 7240 additions and 541 deletions
@@ -0,0 +1,119 @@
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include <cstdint>
#include <string>
#include "fesa/elements/mitc4_shell.h"
#include "fesa/model/model_types.h"
namespace {
using Coordinates = std::array<double, 3>;
fesa::Node Node(std::int64_t label, Coordinates coordinates) {
return {{"I", label, std::to_string(label)}, coordinates, {"mass.inp", 1U}};
}
std::array<const fesa::Node*, 4> Pointers(
const std::array<fesa::Node, 4>& nodes) {
return {&nodes[0], &nodes[1], &nodes[2], &nodes[3]};
}
fesa::Mitc4Shell WarpedShell() {
const std::array<fesa::Node, 4> nodes{
Node(1, {-1.0, -1.0, 0.1}), Node(2, {1.0, -1.0, -0.1}),
Node(3, {1.0, 1.0, 0.2}), Node(4, {-1.0, 1.0, -0.1})};
const std::array<Coordinates, 4> directors{
{{{0.0, 0.0, 1.0}},
{{0.6, 0.0, 0.8}},
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}},
{{0.0, 0.6, 0.8}}}};
const fesa::ShellSection section{"S", 0.5, 0U, {"mass.inp", 2U}};
const fesa::LinearElasticMaterial material{"M", 12.0, 0.0, {"mass.inp", 3U}};
const auto result =
fesa::Mitc4Shell::Create(Pointers(nodes), directors, section, material);
EXPECT_TRUE(result.HasValue());
return result.Value();
}
double Quadratic(const fesa::Matrix& matrix, const fesa::Vector& vector) {
return vector.Dot(matrix.Multiply(vector));
}
// E2-C02: values derive independently in numerical-review-evidence.md.
TEST(Mitc4ShellMassContract, PreservesWarpedMomentsRowsAndFirstSpatialMoment) {
const auto mass = WarpedShell().Mass(3.0);
ASSERT_TRUE(mass.HasValue());
const auto& value = mass.Value();
EXPECT_NEAR(value.consistent_moments[0U](0U, 0U), 17609.0 / 28800.0, 1e-12);
EXPECT_NEAR(value.consistent_moments[1U](0U, 0U), 11003.0 / 1440000.0, 1e-12);
EXPECT_NEAR(value.consistent_moments[2U](0U, 0U), 88177.0 / 6912000.0, 1e-12);
double total_m = 0.0, total_h = 0.0, total_j = 0.0;
for (const auto& row : value.nodal_moments) {
total_m += row[0U];
total_h += row[1U];
total_j += row[2U];
}
EXPECT_NEAR(total_m, 7743.0 / 1600.0, 1e-12);
EXPECT_NEAR(total_h, 739.0 / 12000.0, 1e-12);
EXPECT_NEAR(total_j, 517.0 / 5120.0, 1e-12);
}
// E2-C03: scalar row-sum lumping retains uniform translation energy and is
// never below the consistent kinetic energy.
TEST(Mitc4ShellMassContract, SatisfiesUniformAndJensenEnergyContracts) {
const auto mass = WarpedShell().Mass(3.0);
ASSERT_TRUE(mass.HasValue());
fesa::Vector translation{24U};
fesa::Vector field{24U};
for (std::size_t node = 0U; node < 4U; ++node) {
translation[6U * node] = 1.0;
field[6U * node] = static_cast<double>(node + 1U);
field[6U * node + 1U] = -0.5 * static_cast<double>(node);
field[6U * node + 3U] = 0.25;
}
EXPECT_NEAR(Quadratic(mass.Value().consistent_global24, translation),
Quadratic(mass.Value().lumped_global24, translation), 1e-12);
EXPECT_GE(Quadratic(mass.Value().lumped_global24, field) + 1e-12,
Quadratic(mass.Value().consistent_global24, field));
}
// E2-C04: assemble the physical velocity work over successively finer meshes.
TEST(Mitc4ShellMassContract, ReproducesAffineSquareVelocityWork) {
const std::array<Coordinates, 4> directors{{{{0.0, 0.0, 1.0}},
{{0.0, 0.0, 1.0}},
{{0.0, 0.0, 1.0}},
{{0.0, 0.0, 1.0}}}};
const fesa::ShellSection section{"S", 1.0, 0U, {"mass.inp", 2U}};
const fesa::LinearElasticMaterial material{"M", 12.0, 0.0, {"mass.inp", 3U}};
for (const int mesh : {1, 2, 4}) {
double consistent_work = 0.0, lumped_work = 0.0;
const double edge = 2.0 / mesh;
for (int row = 0; row < mesh; ++row) {
for (int column = 0; column < mesh; ++column) {
const double x = -1.0 + edge * column, y = -1.0 + edge * row;
const std::array<fesa::Node, 4> nodes{
Node(1, {x, y, 0.0}), Node(2, {x + edge, y, 0.0}),
Node(3, {x + edge, y + edge, 0.0}), Node(4, {x, y + edge, 0.0})};
const auto shell = fesa::Mitc4Shell::Create(Pointers(nodes), directors,
section, material);
ASSERT_TRUE(shell.HasValue());
const auto mass = shell.Value().Mass(1.0);
ASSERT_TRUE(mass.HasValue());
fesa::Vector velocity{24U};
for (std::size_t node = 0U; node < 4U; ++node)
velocity[6U * node] = nodes[node].coordinates[0U];
consistent_work +=
Quadratic(mass.Value().consistent_global24, velocity);
lumped_work += Quadratic(mass.Value().lumped_global24, velocity);
}
}
EXPECT_NEAR(consistent_work, 4.0 / 3.0, 1e-12) << mesh;
EXPECT_NEAR(lumped_work - consistent_work, 8.0 / (3.0 * mesh * mesh), 1e-12)
<< mesh;
}
}
} // namespace
+216 -17
View File
@@ -53,6 +53,12 @@ Vector3 Cross(const Vector3& left, const Vector3& right) {
left[0] * right[1] - left[1] * right[0]};
}
Vector3 RotateAboutGlobalZ(const Vector3& value) {
constexpr double kInverseSqrtTwo = 0.70710678118654752440;
return {kInverseSqrtTwo * (value[0U] - value[1U]),
kInverseSqrtTwo * (value[0U] + value[1U]), value[2U]};
}
double Norm(const Vector3& value) { return std::sqrt(Dot(value, value)); }
void ExpectVectorNear(const Vector3& actual, const Vector3& expected,
@@ -636,7 +642,6 @@ TEST(Mitc4ShellKernel,
EXPECT_DOUBLE_EQ(stiffness.physical_local20(0U, 0U), 0x1.d555555555554p+6);
EXPECT_DOUBLE_EQ(stiffness.physical_local20(0U, 4U), 0.0);
EXPECT_DOUBLE_EQ(stiffness.physical_global24(2U, 2U), 0x1.aaaaaaaaaaaadp+5);
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 0x1.0d6cffc5beeb5p-4);
EXPECT_DOUBLE_EQ(recovery.strain_energy, 0x1.20a3d70a3d70bp+6);
constexpr std::array<double, 8> kExpectedStrain{
0x1.999999999999ap-4, -0x1.999999999999bp-5, 0x1.9999999999998p-3,
@@ -791,8 +796,7 @@ TEST(Mitc4ShellPatch, ReproducesIndependentMembraneBendingShearAndTwistFields) {
}
// MITC4-KERNEL-005
TEST(Mitc4ShellDrilling,
UsesOnlyEightPositivePhysicalRotationDiagonalsAndFixedFactor) {
TEST(Mitc4ShellDrilling, UsesAllPhysicalRotationDiagonalsAndFixedFactor) {
const std::array<fesa::Node, 4> nodes{
Node(1, {-50.0, -50.0, 0.0}), Node(2, {50.0, -50.0, 0.0}),
Node(3, {50.0, 50.0, 0.0}), Node(4, {-50.0, 50.0, 0.0})};
@@ -803,27 +807,222 @@ TEST(Mitc4ShellDrilling,
ASSERT_TRUE(stiffness_candidate.HasValue());
const auto& stiffness = stiffness_candidate.Value();
double expected_reference = (std::numeric_limits<double>::max)();
double all_diagonal_minimum = (std::numeric_limits<double>::max)();
double expected_sum = 0.0;
for (std::size_t node_index = 0U; node_index < 4U; ++node_index) {
for (std::size_t rotation = 3U; rotation < 5U; ++rotation) {
const std::size_t index = 5U * node_index + rotation;
const double diagonal = stiffness.physical_local20(index, index);
ASSERT_TRUE(std::isfinite(diagonal));
ASSERT_GT(diagonal, 0.0);
expected_reference = (std::min)(expected_reference, diagonal);
}
for (std::size_t component = 0U; component < 5U; ++component) {
const std::size_t index = 5U * node_index + component;
const double diagonal = stiffness.physical_local20(index, index);
if (std::isfinite(diagonal) && diagonal > 0.0) {
all_diagonal_minimum = (std::min)(all_diagonal_minimum, diagonal);
}
ASSERT_GE(diagonal, 0.0);
expected_sum += diagonal;
}
}
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 1.0e-3 * expected_reference);
EXPECT_LT(all_diagonal_minimum, expected_reference);
EXPECT_NE(stiffness.drilling_stiffness, 1.0e-3 * all_diagonal_minimum);
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 1.0e-3 * expected_sum / 8.0);
}
TEST(Mitc4ShellMass, IntegratesFlatConsistentAndScalarRowSumLumpedMass) {
const auto nodes = PlanarNodes();
const auto shell_candidate = fesa::Mitc4Shell::Create(
NodePointers(nodes), Directors(), Section(), Material());
ASSERT_TRUE(shell_candidate.HasValue());
const auto mass_candidate = shell_candidate.Value().Mass(3.0);
ASSERT_TRUE(mass_candidate.HasValue());
const auto& mass = mass_candidate.Value();
const std::array<std::array<double, 4>, 4> pattern{{{{4.0, 2.0, 1.0, 2.0}},
{{2.0, 4.0, 2.0, 1.0}},
{{1.0, 2.0, 4.0, 2.0}},
{{2.0, 1.0, 2.0, 4.0}}}};
for (std::size_t row = 0U; row < 4U; ++row) {
EXPECT_NEAR(mass.nodal_moments[row][0U], 6.0, 1.0e-12);
EXPECT_NEAR(mass.nodal_moments[row][1U], 0.0, 1.0e-12);
EXPECT_NEAR(mass.nodal_moments[row][2U], 2.0, 1.0e-12);
for (std::size_t column = 0U; column < 4U; ++column) {
EXPECT_NEAR(mass.consistent_moments[0U](row, column),
pattern[row][column] * (2.0 / 3.0), 1.0e-12);
EXPECT_NEAR(mass.consistent_moments[1U](row, column), 0.0, 1.0e-12);
EXPECT_NEAR(mass.consistent_moments[2U](row, column),
pattern[row][column] * (2.0 / 9.0), 1.0e-12);
}
for (std::size_t component = 0U; component < 3U; ++component) {
EXPECT_NEAR(
mass.lumped_global24(6U * row + component, 6U * row + component), 6.0,
1.0e-12);
EXPECT_NEAR(mass.lumped_global24(6U * row + 3U + component,
6U * row + 3U + component),
component == 2U ? 0.0 : 2.0, 1.0e-12);
}
}
ExpectSymmetric(mass.consistent_global24);
ExpectSymmetric(mass.lumped_global24);
}
TEST(Mitc4ShellMass, RejectsNonpositiveAndNonfiniteDensity) {
const auto nodes = PlanarNodes();
const auto shell_candidate = fesa::Mitc4Shell::Create(
NodePointers(nodes), Directors(), Section(), Material());
ASSERT_TRUE(shell_candidate.HasValue());
for (const double density :
{0.0, -1.0, std::numeric_limits<double>::infinity()}) {
const auto mass = shell_candidate.Value().Mass(density);
ASSERT_FALSE(mass.HasValue());
ASSERT_EQ(mass.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(mass.GetStatus().Diagnostics()[0U].code, "invalid-shell-mass");
}
}
TEST(Mitc4ShellMass, PreservesFiniteNodalVarianceAcrossDensityScale) {
const auto nodes = PlanarNodes();
const auto shell_candidate = fesa::Mitc4Shell::Create(
NodePointers(nodes), Directors(), Section(), Material());
ASSERT_TRUE(shell_candidate.HasValue());
for (const double density : {1.0e-180, 1.0e180}) {
const auto mass_candidate = shell_candidate.Value().Mass(density);
ASSERT_TRUE(mass_candidate.HasValue());
for (const auto& moments : mass_candidate.Value().nodal_moments) {
EXPECT_TRUE(std::isfinite(moments[0U]));
EXPECT_TRUE(std::isfinite(moments[1U]));
EXPECT_TRUE(std::isfinite(moments[2U]));
EXPECT_GT(moments[2U] - moments[1U] * (moments[1U] / moments[0U]), 0.0);
}
}
}
void ExpectGlobalZCovariance(const fesa::Matrix& original,
const fesa::Matrix& rotated) {
ASSERT_EQ(original.Rows(), 24U);
ASSERT_EQ(original.Columns(), 24U);
ASSERT_EQ(rotated.Rows(), 24U);
ASSERT_EQ(rotated.Columns(), 24U);
constexpr double kInverseSqrtTwo = 0.70710678118654752440;
const std::array<std::array<double, 3>, 3> transform{
{{{kInverseSqrtTwo, -kInverseSqrtTwo, 0.0}},
{{kInverseSqrtTwo, kInverseSqrtTwo, 0.0}},
{{0.0, 0.0, 1.0}}}};
for (std::size_t row = 0U; row < 24U; ++row) {
for (std::size_t column = 0U; column < 24U; ++column) {
double expected = 0.0;
const std::size_t row_offset =
6U * (row / 6U) + (row % 6U < 3U ? 0U : 3U);
const std::size_t column_offset =
6U * (column / 6U) + (column % 6U < 3U ? 0U : 3U);
const std::size_t row_component = row % 3U;
const std::size_t column_component = column % 3U;
for (std::size_t first = 0U; first < 3U; ++first) {
for (std::size_t second = 0U; second < 3U; ++second) {
expected += transform[row_component][first] *
original(row_offset + first, column_offset + second) *
transform[column_component][second];
}
}
EXPECT_NEAR(rotated(row, column), expected, 1.0e-12)
<< "at (" << row << ", " << column << ")";
}
}
}
TEST(Mitc4ShellMass, PreservesIndependentWarpedMomentOracle) {
const std::array<fesa::Node, 4> nodes{
Node(1, {-1.0, -1.0, 0.1}), Node(2, {1.0, -1.0, -0.1}),
Node(3, {1.0, 1.0, 0.2}), Node(4, {-1.0, 1.0, -0.1})};
const std::array<Vector3, 4> directors{{{{0.0, 0.0, 1.0}},
{{0.6, 0.0, 0.8}},
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}},
{{0.0, 0.6, 0.8}}}};
const auto shell_candidate = fesa::Mitc4Shell::Create(
NodePointers(nodes), directors, Section(0.5), Material());
ASSERT_TRUE(shell_candidate.HasValue());
const auto mass_candidate = shell_candidate.Value().Mass(3.0);
ASSERT_TRUE(mass_candidate.HasValue());
const auto& mass = mass_candidate.Value();
EXPECT_NEAR(mass.consistent_moments[0U](0U, 0U), 17609.0 / 28800.0, 1.0e-12);
EXPECT_NEAR(mass.consistent_moments[1U](0U, 0U), 11003.0 / 1440000.0,
1.0e-12);
EXPECT_NEAR(mass.consistent_moments[2U](0U, 0U), 88177.0 / 6912000.0,
1.0e-12);
double total_mass = 0.0;
double total_first_moment = 0.0;
double total_second_moment = 0.0;
for (const auto& moments : mass.nodal_moments) {
total_mass += moments[0U];
total_first_moment += moments[1U];
total_second_moment += moments[2U];
}
EXPECT_NEAR(total_mass, 7743.0 / 1600.0, 1.0e-12);
EXPECT_NEAR(total_first_moment, 739.0 / 12000.0, 1.0e-12);
EXPECT_NEAR(total_second_moment, 517.0 / 5120.0, 1.0e-12);
}
TEST(Mitc4ShellMass, MeetsPartialBoundaryCompleteMatrixTargets) {
const Vector3 tangent_a{0.0, 1.0, 0.0};
const Vector3 tangent_b{-0.8, 0.0, 0.6};
const std::array<fesa::Node, 4> nodes{
Node(1, {-2.0 * tangent_a[0U] - tangent_b[0U],
-2.0 * tangent_a[1U] - tangent_b[1U],
-2.0 * tangent_a[2U] - tangent_b[2U]}),
Node(2, {2.0 * tangent_a[0U] - tangent_b[0U],
2.0 * tangent_a[1U] - tangent_b[1U],
2.0 * tangent_a[2U] - tangent_b[2U]}),
Node(3, {2.0 * tangent_a[0U] + tangent_b[0U],
2.0 * tangent_a[1U] + tangent_b[1U],
2.0 * tangent_a[2U] + tangent_b[2U]}),
Node(4, {-2.0 * tangent_a[0U] + tangent_b[0U],
-2.0 * tangent_a[1U] + tangent_b[1U],
-2.0 * tangent_a[2U] + tangent_b[2U]})};
const auto shell_candidate =
fesa::Mitc4Shell::Create(NodePointers(nodes), Directors({0.6, 0.0, 0.8}),
Section(1.0), Material(12.0, 0.0));
ASSERT_TRUE(shell_candidate.HasValue());
const auto stiffness_candidate = shell_candidate.Value().Stiffness();
const auto mass_candidate = shell_candidate.Value().Mass(1.0);
ASSERT_TRUE(stiffness_candidate.HasValue());
ASSERT_TRUE(mass_candidate.HasValue());
const double complete_stiffness =
stiffness_candidate.Value().stabilized_global24(5U, 5U);
const double lumped_mass = mass_candidate.Value().lumped_global24(5U, 5U);
EXPECT_NEAR(stiffness_candidate.Value().drilling_stiffness, 19.0 / 4800.0,
1.0e-12);
EXPECT_NEAR(complete_stiffness, 10369.0 / 7500.0, 1.0e-12);
EXPECT_NEAR(lumped_mass, 3.0 / 50.0, 1.0e-12);
EXPECT_NEAR(complete_stiffness / lumped_mass, 10369.0 / 450.0, 1.0e-12);
}
TEST(Mitc4ShellMass, RecomputesCompleteMassAndStiffnessCovariantly) {
const std::array<fesa::Node, 4> nodes{
Node(1, {-1.0, -1.0, 0.1}), Node(2, {1.0, -1.0, -0.1}),
Node(3, {1.0, 1.0, 0.2}), Node(4, {-1.0, 1.0, -0.1})};
const std::array<Vector3, 4> directors{{{{0.0, 0.0, 1.0}},
{{0.6, 0.0, 0.8}},
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}},
{{0.0, 0.6, 0.8}}}};
std::array<fesa::Node, 4> rotated_nodes = nodes;
std::array<Vector3, 4> rotated_directors{};
for (std::size_t node = 0U; node < nodes.size(); ++node) {
rotated_nodes[node].coordinates =
RotateAboutGlobalZ(nodes[node].coordinates);
rotated_directors[node] = RotateAboutGlobalZ(directors[node]);
}
const auto original = fesa::Mitc4Shell::Create(NodePointers(nodes), directors,
Section(0.5), Material());
const auto rotated = fesa::Mitc4Shell::Create(
NodePointers(rotated_nodes), rotated_directors, Section(0.5), Material());
ASSERT_TRUE(original.HasValue());
ASSERT_TRUE(rotated.HasValue());
const auto original_stiffness = original.Value().Stiffness();
const auto rotated_stiffness = rotated.Value().Stiffness();
const auto original_mass = original.Value().Mass(3.0);
const auto rotated_mass = rotated.Value().Mass(3.0);
ASSERT_TRUE(original_stiffness.HasValue());
ASSERT_TRUE(rotated_stiffness.HasValue());
ASSERT_TRUE(original_mass.HasValue());
ASSERT_TRUE(rotated_mass.HasValue());
ExpectGlobalZCovariance(original_stiffness.Value().stabilized_global24,
rotated_stiffness.Value().stabilized_global24);
ExpectGlobalZCovariance(original_mass.Value().consistent_global24,
rotated_mass.Value().consistent_global24);
ExpectGlobalZCovariance(original_mass.Value().lumped_global24,
rotated_mass.Value().lumped_global24);
}
// MITC4-KERNEL-006