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,165 @@
#include "eigen_reference_comparison.h"
#include <gtest/gtest.h>
#include <hdf5.h>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include "fesa/io/abaqus/input_reader.h"
// The direct fixture is intentionally kept in this test file: it exercises the
// comparator contract without depending on the application or HDF5 writer.
namespace {
std::filesystem::path UniqueFixtureDirectory() {
static std::atomic<std::uint64_t> serial{0U};
const auto ticks =
std::chrono::steady_clock::now().time_since_epoch().count();
return std::filesystem::temp_directory_path() /
("fesa-eigen-direct-" + std::to_string(ticks) + "-" +
std::to_string(serial.fetch_add(1U)));
}
struct Mode {
std::uint64_t mode_id;
double eigenvalue;
std::uint8_t classification;
std::uint64_t cluster_id;
};
void Str(hid_t o, const char* n, const std::string& v) {
hid_t t = H5Tcopy(H5T_C_S1);
H5Tset_size(t, H5T_VARIABLE);
H5Tset_cset(t, H5T_CSET_UTF8);
hid_t s = H5Screate(H5S_SCALAR),
a = H5Acreate2(o, n, t, s, H5P_DEFAULT, H5P_DEFAULT);
const char* p = v.c_str();
H5Awrite(a, t, &p);
H5Aclose(a);
H5Sclose(s);
H5Tclose(t);
}
void U(hid_t o, const char* n, std::uint64_t v) {
hid_t s = H5Screate(H5S_SCALAR),
a = H5Acreate2(o, n, H5T_STD_U64LE, s, H5P_DEFAULT, H5P_DEFAULT);
H5Awrite(a, H5T_NATIVE_UINT64, &v);
H5Aclose(a);
H5Sclose(s);
}
TEST(EigenReferenceComparison,
DirectSchemaFixtureAcceptsRepeatedReorderedModes) {
const auto d = UniqueFixtureDirectory();
std::error_code e;
std::filesystem::create_directories(d, e);
const auto inp = d / "x.inp", txt = d / "x.txt", h5 = d / "x.h5";
std::ofstream(inp) << "*Part,name=P\n*Node\n1,0,0,0\n2,1,0,0\n3,1,1,0\n4,0,1,"
"0\n*Element,type=S4\n1,1,2,3,4\n*Elset,elset=E\n1\n*"
"Shell Section,elset=E,material=M\n1.\n*End "
"Part\n*Assembly,name=A\n*Instance,name=I,part=P\n*End "
"Instance\n*Nset,nset=R,instance=I\n1\n*End "
"Assembly\n*Material,name=M\n*Density\n1.\n*Elastic\n1."
",0.3\n*Boundary\nR,1,6\n*Step,name=Step-2,nlgeom=NO\n*"
"Frequency\n2\n*End Step\n";
std::ofstream(txt)
<< "Frame: Mode 2: Value = 100 Freq = 1e999999 (cycles/time)\nFrame: "
"Mode 1: Value = 100 Freq = 1 (cycles/time)\n";
auto p = fesa::AbaqusInputReader{}.Read(inp);
ASSERT_TRUE(p.HasValue());
hid_t f = H5Fcreate(h5.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT,
H5P_DEFAULT),
m = H5Gcreate2(f, "/metadata", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
U(m, "schema_version", 0);
U(m, "eigen_schema_version", 1);
Str(m, "feature_id", "mitc4-shell-feast-eigen2");
Str(m, "analysis_procedure", "linear-frequency");
Str(m, "mass_variant", "derived-lumped");
Str(m, "normalization", "mass");
Str(m, "eigensolver_backend", "MKL FEAST");
Str(m, "source_input_identity",
"path=" + p.Value().source_path.generic_string() +
";content_identity=" + p.Value().source_content_identity);
H5Gclose(m);
hid_t ss = H5Gcreate2(f, "/steps", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
s = H5Gcreate2(ss, "Step-1", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
Str(s, "source_step_name", "Step-2");
U(s, "requested_mode_count", 2);
U(s, "returned_mode_count", 2);
hid_t g = H5Gcreate2(s, "eigen", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
hsize_t n = 2;
hid_t sp = H5Screate_simple(1, &n, nullptr),
t = H5Tcreate(H5T_COMPOUND, sizeof(Mode));
H5Tinsert(t, "mode_id", HOFFSET(Mode, mode_id), H5T_STD_U64LE);
H5Tinsert(t, "eigenvalue", HOFFSET(Mode, eigenvalue), H5T_IEEE_F64LE);
H5Tinsert(t, "classification", HOFFSET(Mode, classification), H5T_STD_U8LE);
H5Tinsert(t, "cluster_id", HOFFSET(Mode, cluster_id), H5T_STD_U64LE);
hid_t ds =
H5Dcreate2(g, "modes", t, sp, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
Mode v[] = {{1, 100, 1, 1}, {2, 100, 1, 1}};
H5Dwrite(ds, t, H5S_ALL, H5S_ALL, H5P_DEFAULT, v);
U(ds, "eigen_schema_version", 1);
Str(ds, "source_step_name", "Step-2");
Str(ds, "domain", "mode");
Str(ds, "quantity", "eigenvalue");
Str(ds, "component_names", "Value");
Str(ds, "component_unit_dimensions", "time^-2");
Str(ds, "coordinate_system", "scalar");
Str(ds, "location", "modal-frame");
H5Dclose(ds);
H5Tclose(t);
H5Sclose(sp);
H5Gclose(g);
H5Gclose(s);
H5Gclose(ss);
H5Fclose(f);
auto reference_case = fesa::test::EigenReferenceCase{"x", inp, txt, h5};
auto ok = fesa::test::EigenReferenceComparison::Compare(reference_case);
ASSERT_TRUE(ok.HasValue());
EXPECT_EQ(ok.Value().families.size(), 2U);
struct TextMutation {
std::string text;
std::string expected_code;
};
const std::vector<TextMutation> invalid_texts{
{"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n",
"eigen-mode-identity-mismatch"},
{"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n"
"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n",
"eigen-mode-identity-mismatch"},
{"Frame: Mode 1: Value = nan Freq = 1 (cycles/time)\n"
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
"invalid-eigen-reference-text"},
{"Frame: Mode 184467440737095516160: Value = 100 Freq = 1 (cycles/time)\n"
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
"eigen-mode-identity-mismatch"},
{"Frame: Mode -1: Value = 100 Freq = 1 (cycles/time)\n"
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
"invalid-eigen-reference-text"},
{"Frame: Mode 3: Value = 100 Freq = 1 (cycles/time)\n"
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
"eigen-mode-identity-mismatch"}};
for (const TextMutation& invalid_text : invalid_texts) {
std::ofstream(txt, std::ios::trunc) << invalid_text.text;
const auto comparison =
fesa::test::EigenReferenceComparison::Compare(reference_case);
ASSERT_FALSE(comparison.HasValue());
ASSERT_EQ(comparison.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(comparison.GetStatus().Diagnostics()[0U].code,
invalid_text.expected_code);
}
std::ofstream(txt, std::ios::trunc)
<< "Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\nFrame: Mode 2: "
"Value = 100 Freq = 1 (cycles/time)\n";
f = H5Fopen(h5.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
m = H5Gopen2(f, "/metadata", H5P_DEFAULT);
H5Adelete(m, "source_input_identity");
Str(m, "source_input_identity", "wrong");
H5Gclose(m);
H5Fclose(f);
EXPECT_FALSE(
fesa::test::EigenReferenceComparison::Compare(reference_case).HasValue());
std::filesystem::remove_all(d, e);
}
} // namespace