#include "io/hdf5/thermal_hdf5_self_check.h" #include #include #include #include #include #include #include "fesa/build_info.h" #include "io/hdf5/hdf5_primitives.h" #include "io/hdf5/hdf5_raii.h" namespace fesa::hdf5_internal { namespace { using U64 = std::uint64_t; /// @brief Rejects malformed mandatory schema without exposing HDF5 diagnostics. void Require(bool condition, const std::string& message) { if (!condition) throw Hdf5Failure{message}; } /// @brief Opens one mandatory dataset and checks its entire declared extent. Hdf5Handle Dataset(hid_t file, const std::string& path, const std::vector& shape) { Hdf5Handle d{ RequireHdf5Id(H5Dopen2(file, path.c_str(), H5P_DEFAULT), path.c_str()), H5Dclose}; Hdf5Handle space{RequireHdf5Id(H5Dget_space(d.Get()), "Missing dataspace."), H5Sclose}; Require( H5Sget_simple_extent_ndims(space.Get()) == static_cast(shape.size()), path + ": wrong rank"); std::vector actual(shape.size()); Require(H5Sget_simple_extent_dims(space.Get(), actual.data(), nullptr) >= 0 && actual == shape, path + ": wrong extent"); return d; } /// @brief Compares every typed scalar against an independent expected /// inventory. template void Values(hid_t d, hid_t file_type, hid_t memory_type, const std::vector& expected) { Hdf5Handle type{RequireHdf5Id(H5Dget_type(d), "Missing dataset type."), H5Tclose}; Require(H5Tequal(type.Get(), file_type) > 0, "Wrong mandatory dataset primitive type."); std::vector values(expected.size()); if (!values.empty()) RequireHdf5( H5Dread(d, memory_type, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), "Cannot read dataset."); Require( values == expected, "Dataset values or ordered identities differ from the supplied history."); } /// @brief Checks a scalar numeric attribute before reading its one-value /// buffer. template void Number(hid_t object, const char* name, hid_t file_type, hid_t memory_type, T expected) { Hdf5Handle a{RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT), name), H5Aclose}; Hdf5Handle t{RequireHdf5Id(H5Aget_type(a.Get()), "Missing attribute type."), H5Tclose}; Hdf5Handle s{RequireHdf5Id(H5Aget_space(a.Get()), "Missing attribute space."), H5Sclose}; Require(H5Tequal(t.Get(), file_type) > 0 && H5Sget_simple_extent_type(s.Get()) == H5S_SCALAR, std::string{name} + ": wrong scalar attribute type/space"); T value{}; RequireHdf5(H5Aread(a.Get(), memory_type, &value), "Cannot read attribute."); Require(value == expected, std::string{name} + ": wrong attribute value"); } /// @brief Reads either fixed or variable UTF-8 strings, preserving schema /// freedom. void Strings(hid_t string_type, const std::vector& expected, const std::function& read, bool compound, const char* field) { Require(H5Tget_class(string_type) == H5T_STRING && H5Tget_cset(string_type) == H5T_CSET_UTF8, "Mandatory string is not UTF-8."); const auto variable = H5Tis_variable_str(string_type); Require(variable >= 0, "Cannot inspect string storage."); const std::size_t width = variable ? sizeof(char*) : H5Tget_size(string_type); Require(width > 0, "Invalid fixed string width."); Hdf5Handle memory; hid_t read_type = string_type; if (compound) { memory = Hdf5Handle{RequireHdf5Id(H5Tcreate(H5T_COMPOUND, width), "Cannot create string projection."), H5Tclose}; RequireHdf5(H5Tinsert(memory.Get(), field, 0, string_type), "Cannot project string field."); read_type = memory.Get(); } bool equal = true; if (variable) { std::vector values(expected.size(), nullptr); const herr_t status = values.empty() ? 0 : read(read_type, values.data()); for (std::size_t i = 0; i < values.size(); ++i) { if (!values[i] || !IsValidUtf8(values[i]) || expected[i] != values[i]) equal = false; if (values[i] && H5free_memory(values[i]) < 0) equal = false; } RequireHdf5(status, "Cannot read variable strings."); } else { std::size_t bytes = 0; Require(SizeProductFits(width, expected.size(), bytes), "String extent overflows memory."); std::vector values(bytes); if (!values.empty()) RequireHdf5(read(read_type, values.data()), "Cannot read fixed strings."); for (std::size_t i = 0; i < expected.size(); ++i) { std::string value(values.data() + i * width, width); const auto end = value.find('\0'); if (end != std::string::npos) value.resize(end); else if (H5Tget_strpad(string_type) == H5T_STR_SPACEPAD) while (!value.empty() && value.back() == ' ') value.pop_back(); if (!IsValidUtf8(value) || value != expected[i]) equal = false; } } Require(equal, "String field/attribute differs from the expected source identity or " "value."); } /// @brief Checks a mandatory scalar UTF-8 attribute, including its exact value. void Text(hid_t object, const char* name, const std::string& expected) { Hdf5Handle a{RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT), name), H5Aclose}; Hdf5Handle t{RequireHdf5Id(H5Aget_type(a.Get()), "Missing text type."), H5Tclose}; Hdf5Handle s{RequireHdf5Id(H5Aget_space(a.Get()), "Missing text space."), H5Sclose}; Require(H5Sget_simple_extent_type(s.Get()) == H5S_SCALAR, "Text attribute is not scalar."); Strings( t.Get(), {expected}, [&](hid_t type, void* data) { return H5Aread(a.Get(), type, data); }, false, name); } /// @brief Locates a named field independently of compound order and padding. Hdf5Handle Field(hid_t dataset, const char* name) { Hdf5Handle t{RequireHdf5Id(H5Dget_type(dataset), "Missing compound type."), H5Tclose}; Require(H5Tget_class(t.Get()) == H5T_COMPOUND, "Mandatory table is not compound."); const int index = H5Tget_member_index(t.Get(), name); Require(index >= 0, std::string{"Missing compound field: "} + name); return { RequireHdf5Id(H5Tget_member_type(t.Get(), static_cast(index)), "Cannot read compound member."), H5Tclose}; } /// @brief Checks typed compound columns, including fixed-array element extents. template void Column(hid_t d, const char* name, const std::vector& expected, hid_t primitive, hid_t native, hsize_t extent = 1) { auto disk = Field(d, name); Hdf5Handle fa, ma; hid_t ft = primitive, mt = native; if (extent != 1) { fa = {RequireHdf5Id(H5Tarray_create2(primitive, 1, &extent), "Cannot create file array projection."), H5Tclose}; ma = {RequireHdf5Id(H5Tarray_create2(native, 1, &extent), "Cannot create memory array projection."), H5Tclose}; ft = fa.Get(); mt = ma.Get(); } Require(H5Tequal(disk.Get(), ft) > 0, std::string{name} + ": wrong compound member type/extent"); Hdf5Handle memory{ RequireHdf5Id( H5Tcreate(H5T_COMPOUND, sizeof(T) * static_cast(extent)), "Cannot create numeric projection."), H5Tclose}; RequireHdf5(H5Tinsert(memory.Get(), name, 0, mt), "Cannot project numeric field."); std::vector values(expected.size()); if (!values.empty()) RequireHdf5( H5Dread(d, memory.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), "Cannot read compound column."); Require(values == expected, std::string{name} + ": compound values or identities differ"); } /// @brief Checks a named string column with independent expected source /// strings. void ColumnText(hid_t d, const char* name, const std::vector& expected) { auto type = Field(d, name); Strings( type.Get(), expected, [&](hid_t t, void* p) { return H5Dread(d, t, H5S_ALL, H5S_ALL, H5P_DEFAULT, p); }, true, name); } /// @brief Checks unsigned identity columns without depending on C++ row layout. void Id(hid_t d, const char* name, const std::vector& expected) { Column(d, name, expected, H5T_STD_U64LE, H5T_NATIVE_UINT64); } /// @brief Checks finite double columns against immutable Domain/history values. void Real(hid_t d, const char* name, const std::vector& expected) { Column(d, name, expected, H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE); } /// @brief Requires exact optional-child absence without treating API errors as /// absence. void Absent(hid_t file, const std::string& parent, const char* child) { const htri_t exists = H5Lexists(file, parent.c_str(), H5P_DEFAULT); Require(exists >= 0, "Cannot inspect optional recovery parent."); if (exists == 0) return; Require(H5Lexists(file, (parent + "/" + child).c_str(), H5P_DEFAULT) == 0, "Initial recovery is present or cannot be inspected."); } /// @brief Checks source tables against Domain rows, including global material /// IDs. void ModelTables(hid_t f, const Domain& domain) { std::vector ids, connectivity, sections, materials, lines; std::vector instances, labels, types, formulations, names, files, elsets; std::vector xyz, k, rho, c, h; for (std::size_t i = 0; i < domain.Nodes().size(); ++i) { const auto& node = domain.Nodes()[i]; ids.push_back(i); instances.push_back(node.source_id.instance_name); labels.push_back(node.source_id.source_label_text); xyz.insert(xyz.end(), node.coordinates.begin(), node.coordinates.end()); } auto nodes = Dataset(f, "/model/nodes", {ids.size()}); Id(nodes.Get(), "internal_node_id", ids); ColumnText(nodes.Get(), "instance_name", instances); ColumnText(nodes.Get(), "source_label", labels); Column(nodes.Get(), "coordinates", xyz, H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, 3); Text(nodes.Get(), "units_label", "length"); ids.clear(); instances.clear(); labels.clear(); for (std::size_t i = 0; i < domain.ThermalElements().Size(); ++i) { const auto& e = domain.ThermalElements()[i]; ids.push_back(i); instances.push_back(e.SourceId().instance_name); labels.push_back(e.SourceId().source_label_text); types.push_back("DS4"); formulations.push_back("FESA-Q4-P2x2-thermal-shell"); for (const auto node : e.NodeIndices()) connectivity.push_back(node); sections.push_back(e.ThermalSectionIndex()); materials.push_back(e.MaterialIndex()); } auto elements = Dataset(f, "/model/elements", {ids.size()}); Id(elements.Get(), "internal_element_id", ids); ColumnText(elements.Get(), "instance_name", instances); ColumnText(elements.Get(), "source_label", labels); ColumnText(elements.Get(), "source_element_type", types); ColumnText(elements.Get(), "internal_formulation", formulations); Column(elements.Get(), "node_internal_ids", connectivity, H5T_STD_U64LE, H5T_NATIVE_UINT64, 4); Id(elements.Get(), "shell_section_internal_id", sections); Id(elements.Get(), "material_internal_id", materials); ids.clear(); for (std::size_t i = 0; i < domain.ThermalMaterials().Size(); ++i) { const auto& material = domain.ThermalMaterials()[i]; ids.push_back(GlobalThermalMaterialId(domain, i)); names.push_back(material.Name()); k.push_back(material.Conductivity()); rho.push_back(material.Density()); c.push_back(material.SpecificHeat()); } auto mat = Dataset(f, "/model/thermal_materials", {ids.size()}); Id(mat.Get(), "internal_material_id", ids); ColumnText(mat.Get(), "name", names); Real(mat.Get(), "conductivity", k); Real(mat.Get(), "density", rho); Real(mat.Get(), "specific_heat", c); Text(mat.Get(), "conductivity_units_label", "power/(length*temperature)"); Text(mat.Get(), "density_units_label", "mass/volume"); Text(mat.Get(), "specific_heat_units_label", "energy/(mass*temperature)"); ids.clear(); materials.clear(); for (std::size_t i = 0; i < domain.ThermalSections().size(); ++i) { const auto& section = domain.ThermalSections()[i]; ids.push_back(i); files.push_back(NormalizedPathString(section.location.file)); lines.push_back(section.location.line); elsets.push_back(section.name); materials.push_back( GlobalThermalMaterialId(domain, section.material_index)); h.push_back(section.thickness); } auto sec = Dataset(f, "/model/shell_sections", {ids.size()}); Id(sec.Get(), "internal_section_id", ids); ColumnText(sec.Get(), "source_file", files); Id(sec.Get(), "source_line", lines); ColumnText(sec.Get(), "source_elset", elsets); Id(sec.Get(), "material_internal_id", materials); Real(sec.Get(), "thickness", h); Text(sec.Get(), "thickness_units_label", "length"); } /// @brief Checks all scalar metadata values, including actual input byte /// identity. void Metadata(hid_t file, const Domain& domain, const ThermalAnalysisState& state) { Hdf5Handle m{RequireHdf5Id(H5Gopen2(file, "/metadata", H5P_DEFAULT), "Missing metadata."), H5Gclose}; Number(m.Get(), "schema_version", H5T_STD_U64LE, H5T_NATIVE_UINT64, U64{1}); Text(m.Get(), "feature_id", "shell-transient-heat-transfer"); Text(m.Get(), "analysis_procedure", "uncoupled-transient-heat-transfer"); Text(m.Get(), "source_step_name", domain.ThermalStep()->name.empty() ? "Step-1" : domain.ThermalStep()->name); Text(m.Get(), "solver_version", std::string{SolverVersion()}); Text(m.Get(), "source_input_identity", SourceInputIdentity(domain)); Text(m.Get(), "unit_system_label", "user-consistent-unspecified"); Text(m.Get(), "internal_formulation", "FESA-Q4-P2x2-thermal-shell"); Text(m.Get(), "integration_rule", "surface-3x3-gauss; thickness-exact-P2x2"); Text(m.Get(), "time_integration", "backward-euler"); Number(m.Get(), "nominal_time_increment", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, state.NominalTimeIncrement()); Number(m.Get(), "time_period", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, state.TimePeriod()); } /// @brief Checks active maps, station/trace conventions and the global-axis R /// layout. void Mappings(hid_t f, const ThermalAnalysisState& s) { const auto u = [&](const char* p, std::vector v) { auto d = Dataset(f, p, {v.size()}); Values(d.Get(), H5T_STD_U64LE, H5T_NATIVE_UINT64, v); }; const auto real = [&](const char* p, std::vector sh, const std::vector& v) { auto d = Dataset(f, p, sh); Values(d.Get(), H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, v); }; u("/model/thermal_node_ids", {s.NodeOrder().begin(), s.NodeOrder().end()}); u("/model/thermal_element_ids", {s.ElementOrder().begin(), s.ElementOrder().end()}); u("/model/temperature_dofs", {11, 12, 13, 14, 15}); real("/model/temperature_zeta", {5}, {-1, -.5, 0, .5, 1}); std::vector mask; for (const auto& row : s.ConstraintMask()) mask.insert(mask.end(), row.begin(), row.end()); auto md = Dataset(f, "/model/thermal_constraint_mask", {s.NodeOrder().size(), 5}); Values(md.Get(), H5T_STD_U8LE, H5T_NATIVE_UINT8, mask); std::vector nodes, dofs; for (const auto& r : s.ReactionDofs()) { nodes.push_back(r.node); dofs.push_back(r.source_dof); } auto reaction = Dataset(f, "/model/thermal_reaction_dofs", {nodes.size()}); Id(reaction.Get(), "internal_node_id", nodes); Id(reaction.Get(), "source_dof", dofs); const double g = 1 / std::sqrt(3.); real("/model/thermal_flux_inplane_coordinates", {4, 2}, {-g, -g, g, -g, g, g, -g, g}); real("/model/thermal_flux_zeta", {6}, {-1, -.5, 0, 0, .5, 1}); auto side = Dataset(f, "/model/thermal_flux_trace_side", {6}); Values(side.Get(), H5T_STD_I8LE, H5T_NATIVE_INT8, std::vector{1, 0, -1, 1, 0, -1}); std::vector frames; for (const auto& element : s.FluxLocalFrames()) for (const auto& point : element) for (std::size_t component = 0; component < 3; ++component) for (std::size_t axis = 0; axis < 3; ++axis) frames.push_back(point[axis][component]); real("/model/thermal_flux_local_frames", {s.ElementOrder().size(), 4, 3, 3}, frames); } /// @brief Checks result values and exact component/unit/location attributes. void ResultValues(hid_t file, const std::string& path, std::vector shape, const std::vector& values, const char* components, const char* units, const char* coordinates, const char* location) { auto dataset = Dataset(file, path, shape); Values(dataset.Get(), H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, values); Text(dataset.Get(), "component_order", components); Text(dataset.Get(), "units_label", units); Text(dataset.Get(), "coordinate_system", coordinates); Text(dataset.Get(), "location", location); } /// @brief Checks frame inventory and actual equation deltas without recomputing /// time differences. void Frames(hid_t f, const ThermalAnalysisState& s) { std::vector index, increment; std::vector time, delta; std::vector initial; for (const auto& row : s.AcceptedFrames()) { index.push_back(row.frame_index); increment.push_back(row.increment_index); time.push_back(row.time); delta.push_back(row.time_increment); initial.push_back(row.is_initial); } auto table = Dataset(f, "/steps/Step-1/accepted_frames", {index.size()}); Id(table.Get(), "frame_index", index); Id(table.Get(), "increment_index", increment); Real(table.Get(), "time", time); Real(table.Get(), "time_increment", delta); Column(table.Get(), "is_initial", initial, H5T_STD_U8LE, H5T_NATIVE_UINT8); Text(table.Get(), "time_units_label", "time"); Hdf5Handle group{ RequireHdf5Id(H5Gopen2(f, "/steps/Step-1/frames", H5P_DEFAULT), "Missing frames group."), H5Gclose}; H5G_info_t info{}; RequireHdf5(H5Gget_info(group.Get(), &info), "Cannot enumerate frames."); Require(info.nlinks == index.size(), "Missing or extra frame groups."); for (const auto& frame : s.AcceptedFrames()) { const auto root = "/steps/Step-1/frames/" + std::to_string(frame.frame_index); Hdf5Handle g{ RequireHdf5Id(H5Gopen2(f, root.c_str(), H5P_DEFAULT), "Missing frame."), H5Gclose}; Number(g.Get(), "frame_index", H5T_STD_U64LE, H5T_NATIVE_UINT64, frame.frame_index); Number(g.Get(), "increment_index", H5T_STD_U64LE, H5T_NATIVE_UINT64, frame.increment_index); Number(g.Get(), "time", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, frame.time); Number(g.Get(), "time_increment", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, frame.time_increment); Number(g.Get(), "is_initial", H5T_STD_U8LE, H5T_NATIVE_UINT8, static_cast(frame.is_initial)); ResultValues(f, root + "/nodal/temperature", {s.NodeOrder().size(), 5}, {frame.temperature.Data(), frame.temperature.Data() + frame.temperature.Size()}, "NT11,NT12,NT13,NT14,NT15", "temperature", "scalar", "nodal-thickness-stations"); if (frame.is_initial) { Absent(f, root + "/nodal", "reaction_heat_rate"); Absent(f, root + "/element", "heat_flux"); } else { const auto& payload = *frame.completed; std::vector reaction(payload.reaction_heat_rate.Size()); for (std::size_t i = 0; i < reaction.size(); ++i) reaction[i] = payload.reaction_heat_rate[i]; ResultValues(f, root + "/nodal/reaction_heat_rate", {reaction.size()}, reaction, "RFL", "power", "scalar", "prescribed-temperature-dofs"); auto rd = Dataset(f, root + "/nodal/reaction_heat_rate", {reaction.size()}); Text(rd.Get(), "positive_direction", "into-model"); std::vector flux; for (const auto& element : payload.global_heat_flux) for (const auto& q : element) for (const double value : q.Components()) flux.push_back(value); ResultValues(f, root + "/element/heat_flux", {s.ElementOrder().size(), 4, 6, 3}, flux, "QX,QY,QZ", "power/area", "global-cartesian", "element-inplane-thickness-traces"); } } } /// @brief Checks sorted diagnostics with all established provenance fields. void Diagnostics(hid_t f, std::vector diagnostics) { SortDiagnostics(diagnostics); std::vector severity, code, file, keyword, entity, message; std::vector line; for (const auto& d : diagnostics) { severity.push_back(d.severity == Severity::kWarning ? "warning" : "error"); code.push_back(d.code); file.push_back(NormalizedPathString(d.location.file)); line.push_back(d.location.line); keyword.push_back(d.keyword); entity.push_back(d.entity_identity); message.push_back(d.message); } auto dataset = Dataset(f, "/diagnostics", {diagnostics.size()}); ColumnText(dataset.Get(), "severity", severity); ColumnText(dataset.Get(), "code", code); ColumnText(dataset.Get(), "file", file); Id(dataset.Get(), "line", line); ColumnText(dataset.Get(), "keyword", keyword); ColumnText(dataset.Get(), "entity_identity", entity); ColumnText(dataset.Get(), "message", message); } } // namespace U64 GlobalThermalMaterialId(const Domain& domain, std::size_t local) { Require(local < domain.ThermalMaterials().Size(), "Invalid local thermal material index."); const auto* thermal = &domain.ThermalMaterials()[local]; for (std::size_t i = 0; i < domain.Materials().Size(); ++i) if (&domain.Materials()[i] == thermal) return i; throw Hdf5Failure{"Thermal material has no global Domain identity."}; } Status ValidateThermalOutput(const Domain& domain, const ThermalAnalysisState& state) { try { Require(state.IsComplete() && domain.ThermalStep() != nullptr && domain.Steps().Size() == 0, "Thermal output requires one complete thermal history."); auto model = AnalysisModel::Create(domain); Require(model.HasValue() && model.Value().IsThermal(), "Invalid thermal Domain."); auto dofs = DofManager::CreateThermal(model.Value()); Require(dofs.HasValue(), "Invalid thermal equation inventory."); Require(state.NodeOrder() == model.Value().ActiveThermalNodes() && state.ElementOrder() == model.Value().ActiveThermalElements(), "Active Domain/state inventories differ."); Require(state.NominalTimeIncrement() == domain.ThermalStep()->nominal_increment && state.TimePeriod() == domain.ThermalStep()->time_period, "Domain/state schedules differ."); const auto grid = ThermalTimeGrid::Create(domain.ThermalStep()->time_period, domain.ThermalStep()->nominal_increment, domain.ThermalStep()->maximum_increments); Require(grid.HasValue() && grid.Value().Count() == state.TimeGrid().Count(), "Domain/state increment inventory differs."); for (std::size_t i = 0; i < state.NodeOrder().size(); ++i) Require(SameIdentity(state.NodeSourceIds()[i], domain.Nodes()[state.NodeOrder()[i]].source_id), "Node source identity differs."); for (std::size_t i = 0; i < state.ElementOrder().size(); ++i) Require(SameIdentity( state.ElementSourceIds()[i], domain.ThermalElements()[state.ElementOrder()[i]].SourceId()), "Element source identity differs."); std::vector> mask(state.NodeOrder().size()); for (const auto row : dofs.Value().ConstrainedDofs()) mask[row / 5][row % 5] = 1; Require(mask == state.ConstraintMask(), "Prescribed temperature inventory differs."); for (const auto& frame : state.AcceptedFrames()) if (!frame.is_initial) for (std::size_t i = 0; i < dofs.Value().ConstrainedDofs().size(); ++i) Require(frame.temperature[dofs.Value().ConstrainedDofs()[i]] == dofs.Value().PrescribedValues()[i], "Completed prescribed temperature differs from Domain."); Vector raw{dofs.Value().FullDofCount()}; for (const auto& initial : domain.ThermalInitialConditions()) { const auto at = std::find(state.NodeOrder().begin(), state.NodeOrder().end(), initial.node_index); if (at != state.NodeOrder().end()) { const auto row = static_cast(at - state.NodeOrder().begin()); for (std::size_t station = 0; station < 5; ++station) raw[5 * row + station] = initial.station_values[station]; } } const auto& actual = state.AcceptedFrames().front().temperature; Require(actual.Size() == raw.Size(), "Raw initial shape differs."); for (std::size_t i = 0; i < raw.Size(); ++i) Require(actual[i] == raw[i], "Raw initial temperatures differ from Domain."); return Status::Ok(); } catch (const std::exception& e) { return OutputFailure("invalid-thermal-output-inventory", e.what()); } } Status SelfCheckThermalFile(const std::filesystem::path& path, const Domain& domain, const ThermalAnalysisState& state, const std::vector& diagnostics, herr_t (*close_reader)(hid_t)) { const auto inventory = ValidateThermalOutput(domain, state); if (!inventory.IsOk()) return inventory; try { Hdf5ErrorSilencer silence; Require(close_reader != nullptr, "Missing thermal reader closer."); Hdf5Handle file{RequireHdf5Id(H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), "Cannot reopen thermal candidate."), close_reader}; Metadata(file.Get(), domain, state); ModelTables(file.Get(), domain); Mappings(file.Get(), state); Frames(file.Get(), state); Diagnostics(file.Get(), diagnostics); RequireHdf5(file.CloseChecked(), "Cannot close checked thermal candidate."); return Status::Ok(); } catch (const std::exception& e) { return OutputFailure("invalid-thermal-hdf5-candidate", e.what()); } } } // namespace fesa::hdf5_internal