#include "thermal_reference_comparison.h" #include #include #include #include #include #include #include #include #include #include #include #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 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 dims(static_cast(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 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 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 std::vector Primitive(hid_t file, const char* path, const std::vector& 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(extent); std::vector 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(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 CompareThermalSnapshots( const ThermalReferenceSnapshot& actual, const ThermalReferenceSnapshot& reference, const std::vector& expected_labels, const std::string& instance) { try { const std::set 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 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::Success(std::move(report)); } catch (const std::exception& error) { return Result::Failure(Failure(error.what())); } } Result 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(std::getline(input, line)), "Missing CSV header."); auto split = [](const std::string& row) { std::vector 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{"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::Success(std::move(result)); } catch (const std::exception& error) { return Result::Failure(Failure(error.what())); } } Result 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(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(file.Id(), "/model/temperature_dofs", {5}, H5T_STD_U64LE, H5T_NATIVE_UINT64) == std::vector{11, 12, 13, 14, 15}, "Wrong station DOFs."); Require(Primitive(file.Id(), "/model/temperature_zeta", {5}, H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE) == std::vector{-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(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 raw(static_cast(shape[0])); Require(H5Dread(nodes.Id(), memory.Id(), H5S_ALL, H5S_ALL, H5P_DEFAULT, raw.data()) >= 0, "Cannot read node identities."); std::map 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(file.Id(), "/model/thermal_node_ids", shape, H5T_STD_U64LE, H5T_NATIVE_UINT64); Require(std::set(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{101}, "Missing/extra reference frames."); std::vector frames(static_cast(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(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(group.Id(), "time", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE) == 37 && Scalar(group.Id(), "frame_index", H5T_STD_U64LE, H5T_NATIVE_UINT64) == 37 && Scalar(group.Id(), "increment_index", H5T_STD_U64LE, H5T_NATIVE_UINT64) == 37 && Scalar(group.Id(), "is_initial", H5T_STD_U8LE, H5T_NATIVE_UINT8) == 0, "Wrong selected frame attributes."); const auto values = Primitive( 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::Success(std::move(result)); } catch (const std::exception& error) { return Result::Failure(Failure(error.what())); } } Result 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::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::Failure( Failure("Unexpected reference input inventory.")); std::vector labels; for (const auto& node : domain.Nodes()) { if (node.source_id.instance_name != "Part-1-1") return Result::Failure( Failure("CSV requires the sole Part-1-1 input instance.")); labels.push_back(static_cast(node.source_id.source_label)); } const auto actual = ReadThermalReferenceHdf5(hdf5), reference = ReadThermalReferenceCsv(csv, "Part-1-1"); if (!actual.HasValue()) return Result::Failure(actual.GetStatus()); if (!reference.HasValue()) return Result::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::Failure( Failure("Wrong source input identity.")); return CompareThermalSnapshots(actual.Value(), reference.Value(), labels, "Part-1-1"); } } // namespace fesa::test