331 lines
13 KiB
C++
331 lines
13 KiB
C++
#include "thermal_reference_comparison.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <hdf5.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
|
|
namespace {
|
|
using fesa::test::ThermalReferenceSnapshot;
|
|
class TemporaryFolder {
|
|
public:
|
|
TemporaryFolder() {
|
|
static std::atomic<unsigned> next{0};
|
|
do {
|
|
path =
|
|
std::filesystem::temp_directory_path() /
|
|
("fesa-thermal-reference-" + std::to_string(std::random_device{}()) +
|
|
"-" + std::to_string(next++));
|
|
} while (!std::filesystem::create_directory(path));
|
|
}
|
|
~TemporaryFolder() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(path, error);
|
|
}
|
|
std::filesystem::path path;
|
|
};
|
|
class Handle {
|
|
public:
|
|
Handle(hid_t id, herr_t (*closer)(hid_t)) : id(id), closer(closer) {
|
|
if (id < 0) throw std::runtime_error{"HDF fixture creation failed."};
|
|
}
|
|
~Handle() { closer(id); }
|
|
Handle(const Handle&) = delete;
|
|
Handle& operator=(const Handle&) = delete;
|
|
hid_t id;
|
|
|
|
private:
|
|
herr_t (*closer)(hid_t);
|
|
};
|
|
void StringAttribute(hid_t object, const char* name, const std::string& value) {
|
|
Handle type{H5Tcopy(H5T_C_S1), H5Tclose};
|
|
ASSERT_GE(H5Tset_size(type.id, H5T_VARIABLE), 0);
|
|
ASSERT_GE(H5Tset_cset(type.id, H5T_CSET_UTF8), 0);
|
|
Handle space{H5Screate(H5S_SCALAR), H5Sclose};
|
|
Handle attr{
|
|
H5Acreate2(object, name, type.id, space.id, H5P_DEFAULT, H5P_DEFAULT),
|
|
H5Aclose};
|
|
const char* text = value.c_str();
|
|
ASSERT_GE(H5Awrite(attr.id, type.id, &text), 0);
|
|
}
|
|
template <class T>
|
|
void ScalarAttribute(hid_t object, const char* name, hid_t type, T value) {
|
|
Handle space{H5Screate(H5S_SCALAR), H5Sclose};
|
|
Handle attr{
|
|
H5Acreate2(object, name, type, space.id, H5P_DEFAULT, H5P_DEFAULT),
|
|
H5Aclose};
|
|
ASSERT_GE(H5Awrite(attr.id, type, &value), 0);
|
|
}
|
|
void Dataset(hid_t file, const char* path, hid_t type,
|
|
const std::vector<hsize_t>& shape, const void* data) {
|
|
Handle space{
|
|
H5Screate_simple(static_cast<int>(shape.size()), shape.data(), nullptr),
|
|
H5Sclose};
|
|
Handle dataset{H5Dcreate2(file, path, type, space.id, H5P_DEFAULT,
|
|
H5P_DEFAULT, H5P_DEFAULT),
|
|
H5Dclose};
|
|
ASSERT_GE(H5Dwrite(dataset.id, type, H5S_ALL, H5S_ALL, H5P_DEFAULT, data), 0);
|
|
}
|
|
// Minimal independent schema fixture for the comparator (not a solver output
|
|
// or a replacement for the immutable Abaqus artifact).
|
|
void HdfFixture(const std::filesystem::path& path, int defect) {
|
|
Handle file{
|
|
H5Fcreate(path.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT),
|
|
H5Fclose};
|
|
for (const auto* name : {"/metadata", "/model", "/steps", "/steps/Step-1",
|
|
"/steps/Step-1/frames", "/steps/Step-1/frames/37",
|
|
"/steps/Step-1/frames/37/nodal"}) {
|
|
Handle group{
|
|
H5Gcreate2(file.id, name, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
|
H5Gclose};
|
|
}
|
|
Handle metadata{H5Gopen2(file.id, "/metadata", H5P_DEFAULT), H5Gclose};
|
|
ScalarAttribute<std::uint64_t>(metadata.id, "schema_version", H5T_STD_U64LE,
|
|
1);
|
|
StringAttribute(metadata.id, "feature_id", "shell-transient-heat-transfer");
|
|
StringAttribute(metadata.id, "analysis_procedure",
|
|
"uncoupled-transient-heat-transfer");
|
|
StringAttribute(metadata.id, "source_step_name", "Step-1");
|
|
StringAttribute(metadata.id, "source_input_identity",
|
|
"synthetic-comparator-fixture");
|
|
StringAttribute(metadata.id, "unit_system_label",
|
|
"user-consistent-unspecified");
|
|
std::uint64_t dofs[5] = {11, 12, 13, 14, 15};
|
|
if (defect == 1) dofs[0] = 15;
|
|
Dataset(file.id, "/model/temperature_dofs", H5T_STD_U64LE, {5}, dofs);
|
|
double zeta[5] = {-1, -.5, 0, .5, 1};
|
|
if (defect == 2) zeta[0] = 0;
|
|
Dataset(file.id, "/model/temperature_zeta", H5T_IEEE_F64LE, {5}, zeta);
|
|
const std::uint64_t active[2] = {1, 0};
|
|
Dataset(file.id, "/model/thermal_node_ids", H5T_STD_U64LE, {2}, active);
|
|
struct Node {
|
|
std::uint64_t id;
|
|
const char* instance;
|
|
const char* label;
|
|
};
|
|
Node nodes[2] = {{0, "Part-1-1", "7"}, {1, "Part-1-1", "20"}};
|
|
if (defect == 9) nodes[1].id = 0;
|
|
if (defect == 10) nodes[1].instance = "Wrong";
|
|
if (defect == 11) nodes[1].label = "7";
|
|
Handle string{H5Tcopy(H5T_C_S1), H5Tclose};
|
|
H5Tset_size(string.id, H5T_VARIABLE);
|
|
H5Tset_cset(string.id, H5T_CSET_UTF8);
|
|
Handle nt{H5Tcreate(H5T_COMPOUND, sizeof(Node)), H5Tclose};
|
|
H5Tinsert(nt.id, "internal_node_id", HOFFSET(Node, id), H5T_STD_U64LE);
|
|
H5Tinsert(nt.id, "instance_name", HOFFSET(Node, instance), string.id);
|
|
if (defect != 14)
|
|
H5Tinsert(nt.id, "source_label", HOFFSET(Node, label), string.id);
|
|
Dataset(file.id, "/model/nodes", nt.id, {2}, nodes);
|
|
struct Frame {
|
|
std::uint64_t frame, increment;
|
|
double time, delta;
|
|
std::uint8_t initial;
|
|
};
|
|
std::vector<Frame> frames;
|
|
for (std::uint64_t i = 0; i < 101; ++i)
|
|
frames.push_back({i, i, static_cast<double>(i), i ? 1. : 0.,
|
|
static_cast<std::uint8_t>(i == 0)});
|
|
if (defect == 6) frames[37].frame = 36;
|
|
if (defect == 7) frames.pop_back();
|
|
if (defect == 8) frames.push_back(frames.back());
|
|
Handle ft{H5Tcreate(H5T_COMPOUND, sizeof(Frame)), H5Tclose};
|
|
H5Tinsert(ft.id, "frame_index", HOFFSET(Frame, frame), H5T_STD_U64LE);
|
|
H5Tinsert(ft.id, "increment_index", HOFFSET(Frame, increment), H5T_STD_U64LE);
|
|
H5Tinsert(ft.id, "time", HOFFSET(Frame, time), H5T_IEEE_F64LE);
|
|
H5Tinsert(ft.id, "time_increment", HOFFSET(Frame, delta), H5T_IEEE_F64LE);
|
|
H5Tinsert(ft.id, "is_initial", HOFFSET(Frame, initial), H5T_STD_U8LE);
|
|
Dataset(file.id, "/steps/Step-1/accepted_frames", ft.id, {frames.size()},
|
|
frames.data());
|
|
Handle group{H5Gopen2(file.id, "/steps/Step-1/frames/37", H5P_DEFAULT),
|
|
H5Gclose};
|
|
ScalarAttribute<double>(group.id, "time", H5T_IEEE_F64LE,
|
|
defect == 12 ? 38. : 37.);
|
|
ScalarAttribute<std::uint64_t>(group.id, "frame_index", H5T_STD_U64LE, 37);
|
|
ScalarAttribute<std::uint64_t>(group.id, "increment_index", H5T_STD_U64LE,
|
|
37);
|
|
ScalarAttribute<std::uint8_t>(group.id, "is_initial", H5T_STD_U8LE, 0);
|
|
double values[10] = {1000, 1, 2, 3, 4, -15.5742216, 5, 6, 7, 8};
|
|
if (defect == 5) values[8] = std::numeric_limits<double>::quiet_NaN();
|
|
Dataset(file.id, "/steps/Step-1/frames/37/nodal/temperature",
|
|
defect == 4 ? H5T_STD_U64LE : H5T_IEEE_F64LE,
|
|
{2, defect == 3 ? 4U : 5U}, values);
|
|
Handle temperature{
|
|
H5Dopen2(file.id, "/steps/Step-1/frames/37/nodal/temperature",
|
|
H5P_DEFAULT),
|
|
H5Dclose};
|
|
StringAttribute(
|
|
temperature.id, "component_order",
|
|
defect == 13 ? "NT15,NT14,NT13,NT12,NT11" : "NT11,NT12,NT13,NT14,NT15");
|
|
StringAttribute(temperature.id, "units_label", "temperature");
|
|
StringAttribute(temperature.id, "coordinate_system", "scalar");
|
|
StringAttribute(temperature.id, "location", "nodal-thickness-stations");
|
|
}
|
|
|
|
TEST(ThermalReferenceComparison,
|
|
HdfSchemaMappingRejectsMalformedStationFrameAndNodeData) {
|
|
TemporaryFolder folder;
|
|
const auto path = folder.path / "fixture.h5";
|
|
for (int defect = 0; defect <= 14; ++defect) {
|
|
SCOPED_TRACE(defect);
|
|
HdfFixture(path, defect);
|
|
const auto read = fesa::test::ReadThermalReferenceHdf5(path);
|
|
if (defect == 0 || defect == 10 || defect == 11) {
|
|
ASSERT_TRUE(read.HasValue())
|
|
<< (read.HasValue() ? "" : read.GetStatus().Diagnostics()[0].message);
|
|
ThermalReferenceSnapshot reference{
|
|
"Step-1",
|
|
37,
|
|
37,
|
|
11,
|
|
{{"Part-1-1", 7, -15.5742216}, {"Part-1-1", 20, 1000}}};
|
|
const auto result = fesa::test::CompareThermalSnapshots(
|
|
read.Value(), reference, {7, 20}, "Part-1-1");
|
|
EXPECT_EQ(result.HasValue(), defect == 0);
|
|
if (result.HasValue()) EXPECT_TRUE(result.Value().passed);
|
|
} else
|
|
EXPECT_FALSE(read.HasValue());
|
|
}
|
|
}
|
|
ThermalReferenceSnapshot Snapshot() {
|
|
return {"Step-1",
|
|
37,
|
|
37,
|
|
11,
|
|
{{"Part-1-1", 7, -15.5742216},
|
|
{"Part-1-1", 20, 1000},
|
|
{"Part-1-1", 3, 0}}};
|
|
}
|
|
TEST(ThermalReferenceComparison,
|
|
MapsSourceLabelsWithoutRowOrderAndPreservesSignedValues) {
|
|
auto actual = Snapshot(), reference = Snapshot();
|
|
std::reverse(actual.rows.begin(), actual.rows.end());
|
|
const auto result = fesa::test::CompareThermalSnapshots(
|
|
actual, reference, {3, 7, 20}, "Part-1-1");
|
|
ASSERT_TRUE(result.HasValue());
|
|
EXPECT_TRUE(result.Value().passed);
|
|
ASSERT_EQ(result.Value().rows.size(), 3U);
|
|
EXPECT_EQ(result.Value().rows[1].label, 7U);
|
|
EXPECT_EQ(result.Value().rows[1].reference, -15.5742216);
|
|
EXPECT_EQ(result.Value().rows[1].tolerance.branch,
|
|
fesa::test::ReferenceToleranceBranch::kRelative);
|
|
EXPECT_EQ(result.Value().family.reference_scale, 1000);
|
|
EXPECT_EQ(result.Value().family.near_zero_band, 10);
|
|
EXPECT_FALSE(result.Value().family.blocking);
|
|
}
|
|
TEST(ThermalReferenceComparison,
|
|
RejectsMissingExtraDuplicateAndNonfiniteBeforeTolerance) {
|
|
for (int mode = 0; mode < 5; ++mode) {
|
|
for (bool mutate_actual : {false, true}) {
|
|
auto actual = Snapshot(), reference = Snapshot();
|
|
auto& rows = mutate_actual ? actual.rows : reference.rows;
|
|
switch (mode) {
|
|
case 0:
|
|
rows.pop_back();
|
|
break;
|
|
case 1:
|
|
rows.push_back({"Part-1-1", 99, 0});
|
|
break;
|
|
case 2:
|
|
rows.push_back(rows[0]);
|
|
break;
|
|
case 3:
|
|
rows[0].value = std::numeric_limits<double>::infinity();
|
|
break;
|
|
default:
|
|
rows[0].value = std::numeric_limits<double>::quiet_NaN();
|
|
break;
|
|
}
|
|
SCOPED_TRACE(mode);
|
|
SCOPED_TRACE(mutate_actual);
|
|
EXPECT_FALSE(fesa::test::CompareThermalSnapshots(actual, reference,
|
|
{3, 7, 20}, "Part-1-1")
|
|
.HasValue());
|
|
}
|
|
}
|
|
}
|
|
TEST(ThermalReferenceComparison,
|
|
RejectsWrongStepTimeIncrementInstanceAndStation) {
|
|
for (int mode = 0; mode < 5; ++mode) {
|
|
for (bool mutate_actual : {false, true}) {
|
|
auto actual = Snapshot(), reference = Snapshot();
|
|
auto& row = mutate_actual ? actual : reference;
|
|
switch (mode) {
|
|
case 0:
|
|
row.step = "Step-2";
|
|
break;
|
|
case 1:
|
|
row.time = std::nextafter(37., 38.);
|
|
break;
|
|
case 2:
|
|
row.increment = 38;
|
|
break;
|
|
case 3:
|
|
row.rows[0].instance = "Other";
|
|
break;
|
|
default:
|
|
row.source_dof = 15;
|
|
break;
|
|
}
|
|
SCOPED_TRACE(mode);
|
|
SCOPED_TRACE(mutate_actual);
|
|
EXPECT_FALSE(fesa::test::CompareThermalSnapshots(actual, reference,
|
|
{3, 7, 20}, "Part-1-1")
|
|
.HasValue());
|
|
}
|
|
}
|
|
}
|
|
TEST(ThermalReferenceComparison,
|
|
UsesUnmodifiedFivePercentOnePercentAndExactZeroPolicy) {
|
|
auto actual = Snapshot(), reference = Snapshot();
|
|
actual.rows[0].value = 0;
|
|
auto result = fesa::test::CompareThermalSnapshots(actual, reference,
|
|
{3, 7, 20}, "Part-1-1");
|
|
ASSERT_TRUE(result.HasValue());
|
|
EXPECT_FALSE(result.Value().passed);
|
|
for (auto& row : reference.rows) row.value = 0;
|
|
actual = reference;
|
|
actual.rows[0].value = std::numeric_limits<double>::denorm_min();
|
|
result = fesa::test::CompareThermalSnapshots(actual, reference, {3, 7, 20},
|
|
"Part-1-1");
|
|
ASSERT_TRUE(result.HasValue());
|
|
EXPECT_FALSE(result.Value().passed);
|
|
EXPECT_EQ(result.Value().family.reference_scale, 0);
|
|
}
|
|
TEST(ThermalReferenceComparison,
|
|
CsvRequiresExactHeaderFrameFiniteValuesAndSoleInstance) {
|
|
static std::atomic<unsigned> next{0};
|
|
std::filesystem::path directory;
|
|
do {
|
|
directory = std::filesystem::temp_directory_path() /
|
|
("fesa-thermal-csv-" + std::to_string(std::random_device{}()) +
|
|
"-" + std::to_string(next++));
|
|
} while (!std::filesystem::create_directory(directory));
|
|
const auto path = directory / "fixture.csv";
|
|
const std::string header = "Step,Frame,Node Label,NT11\n";
|
|
const std::string valid =
|
|
"Step-1,Increment 37: Step Time = 37.00,7,-15.5742216\n";
|
|
for (const auto& text :
|
|
{header + valid, std::string{"Step,Frame,Node Label,NT15\n"} + valid,
|
|
header + "Step-1,Increment 38: Step Time = 37.00,7,0\n",
|
|
header + "Step-2,Increment 37: Step Time = 37.00,7,0\n",
|
|
header + "Step-1,Increment 37: Step Time = 37.00,7,nan\n",
|
|
header + "Step-1,Increment 37: Step Time = 37.00,7,0,extra\n"}) {
|
|
{
|
|
std::ofstream out{path};
|
|
out << text;
|
|
}
|
|
const auto result = fesa::test::ReadThermalReferenceCsv(path, "Part-1-1");
|
|
EXPECT_EQ(result.HasValue(), text == header + valid);
|
|
EXPECT_FALSE(fesa::test::ReadThermalReferenceCsv(path, "").HasValue());
|
|
}
|
|
std::filesystem::remove_all(directory);
|
|
}
|
|
} // namespace
|