#include "eigen_reference_comparison.h" #include #include #include #include #include #include #include #include #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 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 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