194 lines
7.9 KiB
C++
194 lines
7.9 KiB
C++
#include "eigen_reference_comparison.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <hdf5.h>
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <utility>
|
|
#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().policy_id, "mitc4-eigen-first-implementation");
|
|
EXPECT_DOUBLE_EQ(ok.Value().relative_rms_tolerance, 0.05);
|
|
EXPECT_EQ(ok.Value().families.size(), 2U);
|
|
// Only synthetic FESA output is changed; each TXT mode remains an independent
|
|
// scalar family, including at both inclusive five-percent boundaries.
|
|
for (const auto& sample :
|
|
std::vector<std::pair<double, bool>>{
|
|
{103.0, true}, {105.0, true}, {95.0, true},
|
|
{std::nextafter(105.0, std::numeric_limits<double>::infinity()), false},
|
|
{std::nextafter(95.0, 0.0), false}}) {
|
|
SCOPED_TRACE(sample.first);
|
|
f = H5Fopen(h5.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
|
ds = H5Dopen2(f, "/steps/Step-1/eigen/modes", H5P_DEFAULT);
|
|
t = H5Dget_type(ds);
|
|
v[0].eigenvalue = sample.first;
|
|
ASSERT_GE(H5Dwrite(ds, t, H5S_ALL, H5S_ALL, H5P_DEFAULT, v), 0);
|
|
H5Tclose(t);
|
|
H5Dclose(ds);
|
|
H5Fclose(f);
|
|
const auto comparison =
|
|
fesa::test::EigenReferenceComparison::Compare(reference_case);
|
|
ASSERT_TRUE(comparison.HasValue());
|
|
EXPECT_EQ(comparison.Value().passed, sample.second);
|
|
EXPECT_EQ(comparison.Value().families[0].rms_passed, sample.second);
|
|
EXPECT_TRUE(comparison.Value().families[1].passed);
|
|
}
|
|
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
|