Files
FESADev/tests/reference/thermal_reference_comparison.cpp
T

418 lines
18 KiB
C++

#include "thermal_reference_comparison.h"
#include <hdf5.h>
#include <algorithm>
#include <array>
#include <charconv>
#include <cmath>
#include <fstream>
#include <map>
#include <regex>
#include <set>
#include <sstream>
#include <stdexcept>
#include "fesa/analysis/prepared_analysis.h"
namespace fesa::test {
namespace {
Status Failure(const std::string& message) {
return Status::Failure(FailureCategory::kModel,
{{Severity::kError,
"invalid-thermal-reference",
{},
"HEAT TRANSFER",
"shell-thermal/Step-1/t=37/NT11",
message}});
}
void Require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error{message};
}
class H {
public:
H(hid_t id, herr_t (*close)(hid_t)) : id_(id), close_(close) {
Require(id >= 0, "Missing or invalid HDF5 object.");
}
~H() { close_(id_); }
H(const H&) = delete;
H& operator=(const H&) = delete;
hid_t Id() const { return id_; }
private:
hid_t id_;
herr_t (*close_)(hid_t);
};
std::vector<hsize_t> Shape(hid_t dataset) {
H space{H5Dget_space(dataset), H5Sclose};
const int n = H5Sget_simple_extent_ndims(space.Id());
Require(n >= 0, "Invalid dataset dataspace.");
std::vector<hsize_t> dims(static_cast<std::size_t>(n));
Require(H5Sget_simple_extent_dims(space.Id(), dims.data(), nullptr) >= 0,
"Invalid dimensions.");
return dims;
}
std::string Attr(hid_t object, const char* name) {
H a{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
H t{H5Aget_type(a.Id()), H5Tclose};
H s{H5Aget_space(a.Id()), H5Sclose};
Require(H5Sget_simple_extent_type(s.Id()) == H5S_SCALAR &&
H5Tget_class(t.Id()) == H5T_STRING &&
H5Tget_cset(t.Id()) == H5T_CSET_UTF8,
"Invalid UTF8 scalar attribute.");
if (H5Tis_variable_str(t.Id()) > 0) {
char* p = nullptr;
Require(H5Aread(a.Id(), t.Id(), &p) >= 0 && p != nullptr,
"Cannot read string.");
std::string result{p};
H5free_memory(p);
return result;
}
std::vector<char> p(H5Tget_size(t.Id()) + 1, 0);
Require(H5Aread(a.Id(), t.Id(), p.data()) >= 0, "Cannot read fixed string.");
return std::string{p.data()};
}
template <class T>
T Scalar(hid_t object, const char* name, hid_t disk, hid_t memory) {
H a{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
H t{H5Aget_type(a.Id()), H5Tclose};
H s{H5Aget_space(a.Id()), H5Sclose};
Require(H5Tequal(t.Id(), disk) > 0 &&
H5Sget_simple_extent_type(s.Id()) == H5S_SCALAR,
"Invalid scalar type/shape.");
T value{};
Require(H5Aread(a.Id(), memory, &value) >= 0, "Cannot read scalar.");
return value;
}
template <class T>
std::vector<T> Primitive(hid_t file, const char* path,
const std::vector<hsize_t>& dims, hid_t disk,
hid_t memory) {
H d{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
H t{H5Dget_type(d.Id()), H5Tclose};
Require(Shape(d.Id()) == dims && H5Tequal(t.Id(), disk) > 0,
"Wrong dataset primitive/extent.");
std::size_t n = 1;
for (auto extent : dims) n *= static_cast<std::size_t>(extent);
std::vector<T> values(n);
Require(H5Dread(d.Id(), memory, H5S_ALL, H5S_ALL, H5P_DEFAULT,
values.data()) >= 0,
"Cannot read primitive.");
return values;
}
void Field(hid_t compound, const char* name, hid_t expected) {
Require(H5Tget_class(compound) == H5T_COMPOUND, "Expected compound.");
const int index = H5Tget_member_index(compound, name);
Require(index >= 0, "Missing compound field.");
H type{H5Tget_member_type(compound, static_cast<unsigned>(index)), H5Tclose};
Require(H5Tequal(type.Id(), expected) > 0,
"Invalid compound field primitive.");
}
std::uint64_t Label(const std::string& text) {
std::uint64_t value = 0;
const auto parsed =
std::from_chars(text.data(), text.data() + text.size(), value);
Require(parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() &&
value > 0,
"Invalid node label.");
return value;
}
std::string Trim(const std::string& text) {
const auto begin = text.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) return {};
return text.substr(begin, text.find_last_not_of(" \t\r\n") - begin + 1);
}
} // namespace
Result<ThermalReferenceReport> CompareThermalSnapshots(
const ThermalReferenceSnapshot& actual,
const ThermalReferenceSnapshot& reference,
const std::vector<std::uint64_t>& expected_labels,
const std::string& instance) {
try {
const std::set<std::uint64_t> labels(expected_labels.begin(),
expected_labels.end());
Require(!labels.empty() && labels.size() == expected_labels.size() &&
!instance.empty(),
"Invalid input node inventory.");
auto mapped = [&](const ThermalReferenceSnapshot& s) {
Require(s.step == "Step-1" && s.time == 37 && s.increment == 37 &&
s.source_dof == 11,
"Wrong source step/time/increment/station.");
std::map<std::uint64_t, double> result;
for (const auto& row : s.rows) {
Require(row.instance == instance && labels.count(row.label) == 1 &&
std::isfinite(row.value),
"Wrong instance/extra label/nonfinite value.");
Require(result.emplace(row.label, row.value).second,
"Duplicate node identity.");
}
Require(result.size() == labels.size(), "Missing node identity.");
return result;
};
const auto a = mapped(actual), r = mapped(reference);
ReferenceToleranceFamily family{
"shell-thermal/Step-1/time=37/NT11/temperature/user-consistent",
{"NT11"},
false,
{}};
for (auto label : labels) family.rows.push_back({a.at(label), r.at(label)});
const auto evaluation = ReferenceTolerancePolicy::Evaluate({family});
Require(evaluation.valid && evaluation.families.size() == 1,
"Invalid tolerance evaluation.");
ThermalReferenceReport report{
evaluation.families[0].passed, {}, evaluation.families[0]};
std::size_t index = 0;
for (auto label : labels)
report.rows.push_back({instance, label, 11, a.at(label), r.at(label),
report.family.rows[index++]});
return Result<ThermalReferenceReport>::Success(std::move(report));
} catch (const std::exception& error) {
return Result<ThermalReferenceReport>::Failure(Failure(error.what()));
}
}
Result<ThermalReferenceSnapshot> ReadThermalReferenceCsv(
const std::filesystem::path& path, const std::string& sole_instance) {
try {
Require(!sole_instance.empty(), "CSV needs a proven sole input instance.");
std::ifstream input{path};
Require(input.good(), "Cannot read original CSV.");
std::string line;
Require(static_cast<bool>(std::getline(input, line)),
"Missing CSV header.");
auto split = [](const std::string& row) {
std::vector<std::string> values;
std::istringstream in{row};
std::string value;
while (std::getline(in, value, ',')) values.push_back(Trim(value));
return values;
};
Require(split(line) ==
std::vector<std::string>{"Step", "Frame", "Node Label", "NT11"},
"Wrong CSV columns/station.");
ThermalReferenceSnapshot result{"Step-1", 37, 37, 11, {}};
const std::regex frame{R"(^Increment\s+37:\s+Step Time\s*=\s*37\.00$)"};
while (std::getline(input, line)) {
const auto row = split(line);
Require(row.size() == 4 && row[0] == "Step-1" &&
std::regex_match(row[1], frame),
"Wrong CSV source frame.");
std::size_t consumed = 0;
const double value = std::stod(row[3], &consumed);
Require(consumed == row[3].size() && std::isfinite(value),
"Invalid CSV value.");
result.rows.push_back({sole_instance, Label(row[2]), value});
}
Require(input.eof(), "CSV read failed.");
return Result<ThermalReferenceSnapshot>::Success(std::move(result));
} catch (const std::exception& error) {
return Result<ThermalReferenceSnapshot>::Failure(Failure(error.what()));
}
}
Result<ThermalReferenceSnapshot> ReadThermalReferenceHdf5(
const std::filesystem::path& path) {
try {
H file{H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
H meta{H5Gopen2(file.Id(), "/metadata", H5P_DEFAULT), H5Gclose};
Require(
Scalar<std::uint64_t>(meta.Id(), "schema_version", H5T_STD_U64LE,
H5T_NATIVE_UINT64) == 1 &&
Attr(meta.Id(), "feature_id") == "shell-transient-heat-transfer" &&
Attr(meta.Id(), "analysis_procedure") ==
"uncoupled-transient-heat-transfer" &&
Attr(meta.Id(), "source_step_name") == "Step-1" &&
Attr(meta.Id(), "unit_system_label") ==
"user-consistent-unspecified",
"Wrong thermal metadata.");
Require(Primitive<std::uint64_t>(file.Id(), "/model/temperature_dofs", {5},
H5T_STD_U64LE, H5T_NATIVE_UINT64) ==
std::vector<std::uint64_t>{11, 12, 13, 14, 15},
"Wrong station DOFs.");
Require(Primitive<double>(file.Id(), "/model/temperature_zeta", {5},
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE) ==
std::vector<double>{-1, -.5, 0, .5, 1},
"Wrong station coordinates.");
H nodes{H5Dopen2(file.Id(), "/model/nodes", H5P_DEFAULT), H5Dclose};
H nt{H5Dget_type(nodes.Id()), H5Tclose};
const auto shape = Shape(nodes.Id());
Require(shape.size() == 1 && shape[0] > 0, "Invalid nodes shape.");
Field(nt.Id(), "internal_node_id", H5T_STD_U64LE);
struct NodeRow {
std::uint64_t id;
char* instance;
char* label;
};
H string{H5Tcopy(H5T_C_S1), H5Tclose};
H5Tset_size(string.Id(), H5T_VARIABLE);
H5Tset_cset(string.Id(), H5T_CSET_UTF8);
for (const auto* name : {"instance_name", "source_label"}) {
const int index = H5Tget_member_index(nt.Id(), name);
Require(index >= 0, "Missing identity field.");
H type{H5Tget_member_type(nt.Id(), static_cast<unsigned>(index)),
H5Tclose};
Require(H5Tget_class(type.Id()) == H5T_STRING &&
H5Tget_cset(type.Id()) == H5T_CSET_UTF8,
"Wrong identity type.");
}
H memory{H5Tcreate(H5T_COMPOUND, sizeof(NodeRow)), H5Tclose};
H5Tinsert(memory.Id(), "internal_node_id", HOFFSET(NodeRow, id),
H5T_NATIVE_UINT64);
H5Tinsert(memory.Id(), "instance_name", HOFFSET(NodeRow, instance),
string.Id());
H5Tinsert(memory.Id(), "source_label", HOFFSET(NodeRow, label),
string.Id());
std::vector<NodeRow> raw(static_cast<std::size_t>(shape[0]));
Require(H5Dread(nodes.Id(), memory.Id(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
raw.data()) >= 0,
"Cannot read node identities.");
std::map<std::uint64_t, ThermalReferenceRow> identities;
bool valid = true;
for (const auto& row : raw) {
if (!row.instance || !row.label)
valid = false;
else {
try {
valid = identities
.emplace(row.id, ThermalReferenceRow{row.instance,
Label(row.label), 0})
.second &&
valid;
} catch (...) {
valid = false;
}
}
}
H ns{H5Dget_space(nodes.Id()), H5Sclose};
Require(
H5Treclaim(memory.Id(), ns.Id(), H5P_DEFAULT, raw.data()) >= 0 && valid,
"Invalid node identity inventory.");
const auto active =
Primitive<std::uint64_t>(file.Id(), "/model/thermal_node_ids", shape,
H5T_STD_U64LE, H5T_NATIVE_UINT64);
Require(std::set<std::uint64_t>(active.begin(), active.end()).size() ==
active.size(),
"Duplicate active IDs.");
struct Frame {
std::uint64_t frame, increment;
double time, delta;
std::uint8_t initial;
};
H fd{H5Dopen2(file.Id(), "/steps/Step-1/accepted_frames", H5P_DEFAULT),
H5Dclose};
H ft{H5Dget_type(fd.Id()), H5Tclose};
H fm{H5Tcreate(H5T_COMPOUND, sizeof(Frame)), H5Tclose};
for (const auto* name : {"frame_index", "increment_index"})
Field(ft.Id(), name, H5T_STD_U64LE);
for (const auto* name : {"time", "time_increment"})
Field(ft.Id(), name, H5T_IEEE_F64LE);
Field(ft.Id(), "is_initial", H5T_STD_U8LE);
H5Tinsert(fm.Id(), "frame_index", HOFFSET(Frame, frame), H5T_NATIVE_UINT64);
H5Tinsert(fm.Id(), "increment_index", HOFFSET(Frame, increment),
H5T_NATIVE_UINT64);
H5Tinsert(fm.Id(), "time", HOFFSET(Frame, time), H5T_NATIVE_DOUBLE);
H5Tinsert(fm.Id(), "time_increment", HOFFSET(Frame, delta),
H5T_NATIVE_DOUBLE);
H5Tinsert(fm.Id(), "is_initial", HOFFSET(Frame, initial), H5T_NATIVE_UINT8);
const auto fs = Shape(fd.Id());
Require(fs == std::vector<hsize_t>{101}, "Missing/extra reference frames.");
std::vector<Frame> frames(static_cast<std::size_t>(fs[0]));
Require(H5Dread(fd.Id(), fm.Id(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
frames.data()) >= 0,
"Cannot read frames.");
std::size_t matches = 0;
for (std::size_t i = 0; i < frames.size(); ++i) {
const auto& f = frames[i];
Require(f.frame == i && f.increment == i && f.initial == (i == 0) &&
std::isfinite(f.time) && std::isfinite(f.delta),
"Invalid/duplicate frame inventory.");
Require(f.time == static_cast<double>(i) && f.delta == (i == 0 ? 0. : 1.),
"Wrong frame schedule.");
if (f.time == 37) {
++matches;
Require(f.increment == 37, "Wrong reference increment.");
}
}
Require(matches == 1 && frames[37].time == 37,
"No unique exact t=37 frame.");
H group{H5Gopen2(file.Id(), "/steps/Step-1/frames/37", H5P_DEFAULT),
H5Gclose};
Require(Scalar<double>(group.Id(), "time", H5T_IEEE_F64LE,
H5T_NATIVE_DOUBLE) == 37 &&
Scalar<std::uint64_t>(group.Id(), "frame_index", H5T_STD_U64LE,
H5T_NATIVE_UINT64) == 37 &&
Scalar<std::uint64_t>(group.Id(), "increment_index",
H5T_STD_U64LE, H5T_NATIVE_UINT64) == 37 &&
Scalar<std::uint8_t>(group.Id(), "is_initial", H5T_STD_U8LE,
H5T_NATIVE_UINT8) == 0,
"Wrong selected frame attributes.");
const auto values = Primitive<double>(
file.Id(), "/steps/Step-1/frames/37/nodal/temperature", {shape[0], 5},
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE);
ThermalReferenceSnapshot result{"Step-1", 37, 37, 11, {}};
result.source_input_identity = Attr(meta.Id(), "source_input_identity");
H temperature{
H5Dopen2(file.Id(), "/steps/Step-1/frames/37/nodal/temperature",
H5P_DEFAULT),
H5Dclose};
Require(
Attr(temperature.Id(), "component_order") ==
"NT11,NT12,NT13,NT14,NT15" &&
Attr(temperature.Id(), "units_label") == "temperature" &&
Attr(temperature.Id(), "coordinate_system") == "scalar" &&
Attr(temperature.Id(), "location") == "nodal-thickness-stations",
"Wrong temperature component/units/location metadata.");
for (double value : values)
Require(std::isfinite(value), "Nonfinite temperature station.");
for (std::size_t i = 0; i < active.size(); ++i) {
Require(identities.count(active[i]) == 1, "Unknown active node ID.");
auto row = identities.at(active[i]);
row.value = values[5 * i];
result.rows.push_back(std::move(row));
}
return Result<ThermalReferenceSnapshot>::Success(std::move(result));
} catch (const std::exception& error) {
return Result<ThermalReferenceSnapshot>::Failure(Failure(error.what()));
}
}
Result<ThermalReferenceReport> CompareThermalReferenceFiles(
const std::filesystem::path& input, const std::filesystem::path& csv,
const std::filesystem::path& hdf5) {
auto prepared = PrepareAnalysisInput(input);
if (!prepared.HasValue())
return Result<ThermalReferenceReport>::Failure(prepared.GetStatus());
const auto& domain = prepared.Value().domain;
if (!domain.ThermalStep() || domain.ThermalStep()->name != "Step-1" ||
domain.Nodes().size() != 729 || domain.ThermalElements().Size() != 676)
return Result<ThermalReferenceReport>::Failure(
Failure("Unexpected reference input inventory."));
std::vector<std::uint64_t> labels;
for (const auto& node : domain.Nodes()) {
if (node.source_id.instance_name != "Part-1-1")
return Result<ThermalReferenceReport>::Failure(
Failure("CSV requires the sole Part-1-1 input instance."));
labels.push_back(static_cast<std::uint64_t>(node.source_id.source_label));
}
const auto actual = ReadThermalReferenceHdf5(hdf5),
reference = ReadThermalReferenceCsv(csv, "Part-1-1");
if (!actual.HasValue())
return Result<ThermalReferenceReport>::Failure(actual.GetStatus());
if (!reference.HasValue())
return Result<ThermalReferenceReport>::Failure(reference.GetStatus());
const std::string expected_identity =
"path=" +
std::filesystem::absolute(domain.SourcePath())
.lexically_normal()
.generic_u8string() +
";content_identity=" + domain.SourceContentIdentity();
if (actual.Value().source_input_identity != expected_identity)
return Result<ThermalReferenceReport>::Failure(
Failure("Wrong source input identity."));
return CompareThermalSnapshots(actual.Value(), reference.Value(), labels,
"Part-1-1");
}
} // namespace fesa::test