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:
@@ -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
|
||||
Reference in New Issue
Block a user