#include "fesa/io/hdf5/thermal_hdf5_results_writer.h" #include #include #include #include #include #include #include "fesa/build_info.h" #include "io/hdf5/hdf5_atomic_file.h" #include "io/hdf5/hdf5_model_writer.h" #include "io/hdf5/hdf5_primitives.h" #include "io/hdf5/hdf5_raii.h" #include "io/hdf5/hdf5_result_writer.h" #include "io/hdf5/thermal_hdf5_self_check.h" namespace fesa { namespace { using hdf5_internal::Hdf5Handle; /// @brief Carries node/source-DOF reaction identities in stable row order. struct ReactionRow { std::uint64_t internal_node_id; std::uint64_t source_dof; }; /// @brief Carries accepted schedule values independently of stored-time /// differences. struct FrameRow { std::uint64_t frame_index; std::uint64_t increment_index; double time; double time_increment; std::uint8_t is_initial; }; /// @brief Carries thermal properties with global Domain material identities. struct MaterialRow { std::uint64_t internal_material_id; const char* name; double conductivity; double density; double specific_heat; }; /// @brief Carries section provenance and global material linkage. struct SectionRow { std::uint64_t internal_section_id; const char* source_file; std::uint64_t source_line; const char* source_elset; std::uint64_t material_internal_id; double thickness; }; /// @brief Carries DS4 source identities and fixed four-node connectivity. struct ElementRow { std::uint64_t internal_element_id; const char* instance_name; const char* source_label; const char* source_element_type; const char* internal_formulation; std::uint64_t node_internal_ids[4]; std::uint64_t shell_section_internal_id; std::uint64_t material_internal_id; }; /// @brief Writes fixed named fields, preserving zero-length reaction tables. template Hdf5Handle Compound(hid_t file, const char* path, const std::vector& rows, hid_t file_type, hid_t memory_type) { if (rows.empty()) { const hsize_t zero[] = {0U}; const hsize_t unlimited[] = {H5S_UNLIMITED}; Hdf5Handle space{ hdf5_internal::RequireHdf5Id(H5Screate_simple(1, zero, unlimited), "Unable to create empty compound space."), H5Sclose}; Hdf5Handle properties{hdf5_internal::RequireHdf5Id( H5Pcreate(H5P_DATASET_CREATE), "Unable to create empty compound properties."), H5Pclose}; const hsize_t chunk[] = {1U}; hdf5_internal::RequireHdf5(H5Pset_chunk(properties.Get(), 1, chunk), "Unable to configure empty compound chunk."); return Hdf5Handle{ hdf5_internal::RequireHdf5Id( H5Dcreate2(file, path, file_type, space.Get(), H5P_DEFAULT, properties.Get(), H5P_DEFAULT), "Unable to create empty compound HDF5 dataset."), H5Dclose}; } return hdf5_internal::WriteCompoundDataset(file, path, rows.size(), file_type, memory_type, rows.data()); } /// @brief Defines an explicit named field without making padding a read /// contract. void Member(hid_t t, const char* n, size_t off, hid_t f) { hdf5_internal::RequireHdf5(H5Tinsert(t, n, off, f), "Unable to define compound field."); } /// @brief Builds fixed-width connectivity array datatypes. Hdf5Handle Array(hid_t type, int rank, const hsize_t* dims) { return Hdf5Handle{ hdf5_internal::RequireHdf5Id(H5Tarray_create2(type, rank, dims), "Unable to create array type."), H5Tclose}; } /// @brief Separates fixed little-endian file fields from native memory fields. template void Types(Hdf5Handle& ft, Hdf5Handle& mt, Init init) { ft = Hdf5Handle{ hdf5_internal::RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(Row)), "Unable to create file compound."), H5Tclose}; mt = Hdf5Handle{ hdf5_internal::RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(Row)), "Unable to create memory compound."), H5Tclose}; init(ft.Get(), mt.Get()); } /// @brief Writes all twelve thermal discriminator, source and schedule /// attributes. void Metadata(hid_t file, const Domain& domain, const ThermalAnalysisState& state) { auto g = hdf5_internal::CreateGroup(file, "/metadata"); auto a = [&](const char* n, const std::string& v) { hdf5_internal::WriteStringAttribute(g.Get(), n, v); }; hdf5_internal::WriteUint64Attribute(g.Get(), "schema_version", 1); a("feature_id", "shell-transient-heat-transfer"); a("analysis_procedure", "uncoupled-transient-heat-transfer"); a("source_step_name", domain.ThermalStep()->name.empty() ? "Step-1" : domain.ThermalStep()->name); a("solver_version", std::string{SolverVersion()}); a("source_input_identity", hdf5_internal::SourceInputIdentity(domain)); a("unit_system_label", "user-consistent-unspecified"); a("internal_formulation", "FESA-Q4-P2x2-thermal-shell"); a("integration_rule", "surface-3x3-gauss; thickness-exact-P2x2"); a("time_integration", "backward-euler"); hdf5_internal::WriteDoubleAttribute(g.Get(), "nominal_time_increment", state.NominalTimeIncrement()); hdf5_internal::WriteDoubleAttribute(g.Get(), "time_period", state.TimePeriod()); } /// @brief Writes all geometry with separate active thermal row maps and /// recovery axes. void WriteThermalModel(hid_t file, const Domain& d, const ThermalAnalysisState& s) { auto model = hdf5_internal::CreateGroup(file, "/model"); (void)model; // Nodes retain all Domain rows; active result mappings remain separate. Hdf5Handle ft, mt; auto st = hdf5_internal::MakeUtf8StringType(); hdf5_internal::WriteNodes(file, d); std::vector elements; for (size_t i = 0; i < d.ThermalElements().Size(); ++i) { const auto& e = d.ThermalElements()[i]; elements.push_back({i, e.SourceId().instance_name.c_str(), e.SourceId().source_label_text.c_str(), "DS4", "FESA-Q4-P2x2-thermal-shell", {e.NodeIndices()[0], e.NodeIndices()[1], e.NodeIndices()[2], e.NodeIndices()[3]}, e.ThermalSectionIndex(), e.MaterialIndex()}); } hsize_t four[] = {4}; auto fu = Array(H5T_STD_U64LE, 1, four), mu = Array(H5T_NATIVE_UINT64, 1, four); Types(ft, mt, [&](hid_t f, hid_t m) { auto add = [&](hid_t t, hid_t u, hid_t ar) { Member(t, "internal_element_id", HOFFSET(ElementRow, internal_element_id), u); Member(t, "instance_name", HOFFSET(ElementRow, instance_name), st.Get()); Member(t, "source_label", HOFFSET(ElementRow, source_label), st.Get()); Member(t, "source_element_type", HOFFSET(ElementRow, source_element_type), st.Get()); Member(t, "internal_formulation", HOFFSET(ElementRow, internal_formulation), st.Get()); Member(t, "node_internal_ids", HOFFSET(ElementRow, node_internal_ids), ar); Member(t, "shell_section_internal_id", HOFFSET(ElementRow, shell_section_internal_id), u); Member(t, "material_internal_id", HOFFSET(ElementRow, material_internal_id), u); }; add(f, H5T_STD_U64LE, fu.Get()); add(m, H5T_NATIVE_UINT64, mu.Get()); }); (void)Compound(file, "/model/elements", elements, ft.Get(), mt.Get()); std::vector mats; for (size_t i = 0; i < d.ThermalMaterials().Size(); ++i) { const auto& m = d.ThermalMaterials()[i]; size_t id = 0; for (; id < d.Materials().Size(); ++id) if (&d.Materials()[id] == &m) break; mats.push_back({id, m.Name().c_str(), m.Conductivity(), m.Density(), m.SpecificHeat()}); } Types(ft, mt, [&](hid_t f, hid_t m) { auto add = [&](hid_t t, hid_t u, hid_t q) { Member(t, "internal_material_id", HOFFSET(MaterialRow, internal_material_id), u); Member(t, "name", HOFFSET(MaterialRow, name), st.Get()); Member(t, "conductivity", HOFFSET(MaterialRow, conductivity), q); Member(t, "density", HOFFSET(MaterialRow, density), q); Member(t, "specific_heat", HOFFSET(MaterialRow, specific_heat), q); }; add(f, H5T_STD_U64LE, H5T_IEEE_F64LE); add(m, H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); }); auto md = Compound(file, "/model/thermal_materials", mats, ft.Get(), mt.Get()); hdf5_internal::WriteStringAttribute(md.Get(), "conductivity_units_label", "power/(length*temperature)"); hdf5_internal::WriteStringAttribute(md.Get(), "density_units_label", "mass/volume"); hdf5_internal::WriteStringAttribute(md.Get(), "specific_heat_units_label", "energy/(mass*temperature)"); std::vector secs; std::vector files; files.reserve(d.ThermalSections().size()); for (size_t i = 0; i < d.ThermalSections().size(); ++i) { const auto& q = d.ThermalSections()[i]; files.push_back(hdf5_internal::NormalizedPathString(q.location.file)); secs.push_back({i, files.back().c_str(), q.location.line, q.name.c_str(), hdf5_internal::GlobalThermalMaterialId(d, q.material_index), q.thickness}); } Types(ft, mt, [&](hid_t f, hid_t m) { auto add = [&](hid_t t, hid_t u, hid_t q) { Member(t, "internal_section_id", HOFFSET(SectionRow, internal_section_id), u); Member(t, "source_file", HOFFSET(SectionRow, source_file), st.Get()); Member(t, "source_line", HOFFSET(SectionRow, source_line), u); Member(t, "source_elset", HOFFSET(SectionRow, source_elset), st.Get()); Member(t, "material_internal_id", HOFFSET(SectionRow, material_internal_id), u); Member(t, "thickness", HOFFSET(SectionRow, thickness), q); }; add(f, H5T_STD_U64LE, H5T_IEEE_F64LE); add(m, H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); }); auto sd = Compound(file, "/model/shell_sections", secs, ft.Get(), mt.Get()); hdf5_internal::WriteStringAttribute(sd.Get(), "thickness_units_label", "length"); std::vector nids(s.NodeOrder().begin(), s.NodeOrder().end()), eids(s.ElementOrder().begin(), s.ElementOrder().end()); auto U = [&](const char* p, const std::vector& v) { auto sp = hdf5_internal::CreateDatasetSpace({v.size()}); Hdf5Handle ds{hdf5_internal::RequireHdf5Id( H5Dcreate2(file, p, H5T_STD_U64LE, sp.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), "Unable to create IDs."), H5Dclose}; if (!v.empty()) hdf5_internal::RequireHdf5(H5Dwrite(ds.Get(), H5T_NATIVE_UINT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, v.data()), "Unable to write IDs."); }; U("/model/thermal_node_ids", nids); U("/model/thermal_element_ids", eids); std::vector dofs = {11, 12, 13, 14, 15}; U("/model/temperature_dofs", dofs); const std::vector zeta = {-1, -.5, 0, .5, 1}, coords = {-1 / std::sqrt(3.), -1 / std::sqrt(3.), 1 / std::sqrt(3.), -1 / std::sqrt(3.), 1 / std::sqrt(3.), 1 / std::sqrt(3.), -1 / std::sqrt(3.), 1 / std::sqrt(3.)}, fz = {-1, -.5, 0, 0, .5, 1}; auto D = [&](const char* p, std::vector sh, const std::vector& v) { (void)hdf5_internal::WriteDoubleValues(file, p, sh, v.data(), v.size()); }; D("/model/temperature_zeta", {5}, zeta); D("/model/thermal_flux_inplane_coordinates", {4, 2}, coords); D("/model/thermal_flux_zeta", {6}, fz); std::vector side = {1, 0, -1, 1, 0, -1}; auto sp = hdf5_internal::CreateDatasetSpace({6}); Hdf5Handle sid{ hdf5_internal::RequireHdf5Id( H5Dcreate2(file, "/model/thermal_flux_trace_side", H5T_STD_I8LE, sp.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), "Unable to create trace side."), H5Dclose}; hdf5_internal::RequireHdf5(H5Dwrite(sid.Get(), H5T_NATIVE_INT8, H5S_ALL, H5S_ALL, H5P_DEFAULT, side.data()), "Unable to write trace side."); std::vector mask; for (const auto& r : s.ConstraintMask()) mask.insert(mask.end(), r.begin(), r.end()); hdf5_internal::WriteUint8Dataset(file, "/model/thermal_constraint_mask", {s.NodeOrder().size(), 5}, mask.data(), mask.size()); std::vector reactions; for (const auto& r : s.ReactionDofs()) reactions.push_back({r.node, (std::uint64_t)r.source_dof}); Types(ft, mt, [&](hid_t f, hid_t m) { Member(f, "internal_node_id", HOFFSET(ReactionRow, internal_node_id), H5T_STD_U64LE); Member(f, "source_dof", HOFFSET(ReactionRow, source_dof), H5T_STD_U64LE); Member(m, "internal_node_id", HOFFSET(ReactionRow, internal_node_id), H5T_NATIVE_UINT64); Member(m, "source_dof", HOFFSET(ReactionRow, source_dof), H5T_NATIVE_UINT64); }); (void)Compound(file, "/model/thermal_reaction_dofs", reactions, ft.Get(), mt.Get()); std::vector frames; for (const auto& e : s.FluxLocalFrames()) for (const auto& p : e) for (size_t g = 0; g < 3; ++g) for (size_t a = 0; a < 3; ++a) frames.push_back(p[a][g]); D("/model/thermal_flux_local_frames", {s.ElementOrder().size(), 4, 3, 3}, frames); } /// @brief Writes raw initial temperature and completed-only reactions and /// fluxes. void WriteFrames(hid_t file, const ThermalAnalysisState& s) { (void)hdf5_internal::CreateGroup(file, "/steps/Step-1"); std::vector rows; for (const auto& f : s.AcceptedFrames()) rows.push_back({f.frame_index, f.increment_index, f.time, f.time_increment, (std::uint8_t)f.is_initial}); Hdf5Handle ft, mt; Types(ft, mt, [&](hid_t x, hid_t y) { auto a = [&](hid_t t, hid_t u, hid_t q) { Member(t, "frame_index", HOFFSET(FrameRow, frame_index), u); Member(t, "increment_index", HOFFSET(FrameRow, increment_index), u); Member(t, "time", HOFFSET(FrameRow, time), q); Member(t, "time_increment", HOFFSET(FrameRow, time_increment), q); Member(t, "is_initial", HOFFSET(FrameRow, is_initial), H5T_STD_U8LE); }; a(x, H5T_STD_U64LE, H5T_IEEE_F64LE); a(y, H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); }); auto af = Compound(file, "/steps/Step-1/accepted_frames", rows, ft.Get(), mt.Get()); hdf5_internal::WriteStringAttribute(af.Get(), "time_units_label", "time"); for (const auto& f : s.AcceptedFrames()) { std::string root = "/steps/Step-1/frames/" + std::to_string(f.frame_index); auto g = hdf5_internal::CreateGroup(file, root.c_str()); (void)hdf5_internal::CreateGroup(file, (root + "/nodal").c_str()); hdf5_internal::WriteUint64Attribute(g.Get(), "frame_index", f.frame_index); hdf5_internal::WriteUint64Attribute(g.Get(), "increment_index", f.increment_index); hdf5_internal::WriteDoubleAttribute(g.Get(), "time", f.time); hdf5_internal::WriteDoubleAttribute(g.Get(), "time_increment", f.time_increment); hdf5_internal::WriteUint8Attribute(g.Get(), "is_initial", f.is_initial); auto td = hdf5_internal::WriteDoubleValues( file, root + "/nodal/temperature", {s.NodeOrder().size(), 5}, f.temperature.Data(), f.temperature.Size()); hdf5_internal::WriteStringAttribute(td.Get(), "component_order", "NT11,NT12,NT13,NT14,NT15"); hdf5_internal::WriteStringAttribute(td.Get(), "units_label", "temperature"); hdf5_internal::WriteStringAttribute(td.Get(), "coordinate_system", "scalar"); hdf5_internal::WriteStringAttribute(td.Get(), "location", "nodal-thickness-stations"); if (f.completed) { (void)hdf5_internal::CreateGroup(file, (root + "/element").c_str()); auto rd = hdf5_internal::WriteDoubleValues( file, root + "/nodal/reaction_heat_rate", {s.ReactionDofs().size()}, f.completed->reaction_heat_rate.Data(), f.completed->reaction_heat_rate.Size()); hdf5_internal::WriteStringAttribute(rd.Get(), "component_order", "RFL"); hdf5_internal::WriteStringAttribute(rd.Get(), "units_label", "power"); hdf5_internal::WriteStringAttribute(rd.Get(), "coordinate_system", "scalar"); hdf5_internal::WriteStringAttribute(rd.Get(), "location", "prescribed-temperature-dofs"); hdf5_internal::WriteStringAttribute(rd.Get(), "positive_direction", "into-model"); std::vector q; for (const auto& e : f.completed->global_heat_flux) for (size_t p = 0; p < 4; ++p) for (size_t z = 0; z < 6; ++z) for (size_t c = 0; c < 3; ++c) q.push_back(e[6 * p + z][c]); auto fd = hdf5_internal::WriteDoubleValues( file, root + "/element/heat_flux", {s.ElementOrder().size(), 4, 6, 3}, q.data(), q.size()); hdf5_internal::WriteStringAttribute(fd.Get(), "component_order", "QX,QY,QZ"); hdf5_internal::WriteStringAttribute(fd.Get(), "units_label", "power/area"); hdf5_internal::WriteStringAttribute(fd.Get(), "coordinate_system", "global-cartesian"); hdf5_internal::WriteStringAttribute(fd.Get(), "location", "element-inplane-thickness-traces"); } } } /// @brief Converts adapter failures into the existing output-failure category. Status Fail(const char* m) { return hdf5_internal::OutputFailure("thermal-output-schema", m); } } // namespace Status hdf5_internal::WriteThermalFile( const std::filesystem::path& output_path, const Domain& domain, const ThermalAnalysisState& state, const std::vector& diagnostics, const ThermalFileHooks& hooks) { const auto inventory = ValidateThermalOutput(domain, state); if (!inventory.IsOk()) return inventory; if (!hooks.candidate_close || !hooks.reader_close) return Fail("Missing thermal file closer."); try { hdf5_internal::Hdf5ErrorSilencer silence; auto candidate = hdf5_internal::MakeCandidatePath(output_path); hdf5_internal::CandidateFileGuard guard{candidate}; Hdf5Handle file{hdf5_internal::RequireHdf5Id( H5Fcreate(candidate.string().c_str(), H5F_ACC_EXCL, H5P_DEFAULT, H5P_DEFAULT), "Unable to create thermal candidate."), hooks.candidate_close}; if (hooks.before_write) hooks.before_write(file.Get()); Metadata(file.Get(), domain, state); WriteThermalModel(file.Get(), domain, state); WriteFrames(file.Get(), state); hdf5_internal::WriteDiagnostics(file.Get(), diagnostics); hdf5_internal::RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL), "Unable to flush thermal candidate."); hdf5_internal::RequireHdf5(file.CloseChecked(), "Unable to close thermal candidate."); if (hooks.before_self_check) hooks.before_self_check(candidate); const Status ok = hdf5_internal::SelfCheckThermalFile( candidate, domain, state, diagnostics, hooks.reader_close); if (ok.IsOk() && hooks.before_finalize) hooks.before_finalize(candidate); const Status out = hdf5_internal::AtomicFinalizeValidatedCandidate( candidate, output_path, ok); if (out.IsOk()) guard.Release(); return out; } catch (const std::exception& e) { return Fail(e.what()); } } Status ThermalHdf5ResultsWriter::Write( const std::filesystem::path& output_path, const Domain& domain, const ThermalAnalysisState& state, const std::vector& diagnostics) { return hdf5_internal::WriteThermalFile(output_path, domain, state, diagnostics); } } // namespace fesa