#include "fesa/io/hdf5/thermal_hdf5_results_writer.h" #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/assembly/thermal_assembler.h" #include "fesa/fem/dof_manager.h" #include "io/hdf5/hdf5_raii.h" #include "io/hdf5/thermal_hdf5_self_check.h" namespace { fesa::ModelDefinition Definition() { fesa::ModelDefinition m{}; const std::filesystem::path file{"thermal-writer.inp"}; m.source_path = file; m.source_content_identity = "thermal-writer-source"; m.nodes = {{{"unused", 90, "0090"}, {9, 9, 9}, {file, 1}}, {{"part", 7, "0007"}, {0, 0, 0}, {file, 2}}, {{"part", 2, "0002"}, {1, 0, 0}, {file, 3}}, {{"part", 9, "0009"}, {1, 1, 0}, {file, 4}}, {{"part", 1, "0001"}, {0, 1, 0}, {file, 5}}}; // The structural row makes the thermal material's global ID nonzero. m.materials = {{"structural", 1, .25, {file, 6}}}; m.thermal_materials = {{{"part", 1, "1"}, "thermal", 2, 3, 4, {file, 7}}}; m.thermal_sections = {{{"part", 1, "1"}, "ELSET", .2, 0, {file, 8}}}; m.thermal_elements = {{{"part", 13, "0013"}, {1, 2, 3, 4}, 0, {file, 9}}}; m.thermal_initial_conditions = {{1, -2, {file, 10}}}; m.thermal_step = fesa::ThermalStepDefinition{"raw Step", .3, .7, 3}; return m; } struct Fixture { std::unique_ptr domain; fesa::ThermalAnalysisState state; }; Fixture Make(fesa::ModelDefinition definition = Definition(), bool complete = true) { auto d = fesa::Domain::Create(std::move(definition)); if (!d.HasValue()) throw std::runtime_error{"domain"}; auto domain = std::make_unique(std::move(d.Value())); auto m = fesa::AnalysisModel::Create(*domain); auto dofs = fesa::DofManager::CreateThermal(m.Value()); auto a = fesa::AssembleThermal(m.Value(), dofs.Value(), fesa::SerialParallelFor{}); auto state = fesa::ThermalAnalysisState::Create(m.Value(), dofs.Value(), a.Value()); if (!state.HasValue()) throw std::runtime_error{"state"}; for (std::uint32_t i = 1; complete && i <= state.Value().TimeGrid().Count(); ++i) { fesa::Vector t{20U}; fesa::Vector r{state.Value().ReactionDofs().size()}; for (std::size_t row = 0; row < t.Size(); ++row) t[row] = static_cast(100 * i + row) - 200; for (std::size_t row = 0; row < dofs.Value().ConstrainedDofs().size(); ++row) t[dofs.Value().ConstrainedDofs()[row]] = dofs.Value().PrescribedValues()[row]; for (std::size_t row = 0; row < r.Size(); ++row) r[row] = -10. * i + static_cast(row); std::vector q(1); for (auto& e : q) for (std::size_t x = 0; x < 24; ++x) e[x] = fesa::Vector3{double(x), -double(x) - i, 3. * i}; auto frame = state.Value().MakeCompletedFrame(i, std::move(t), std::move(r), std::move(q)); if (!frame.HasValue() || !state.Value().CommitAcceptedFrame(frame.Value()).IsOk()) throw std::runtime_error{"frame"}; } return {std::move(domain), std::move(state.Value())}; } class Temp { public: Temp() { static std::atomic next{0}; do { directory_ = std::filesystem::temp_directory_path() / ("fesa-thermal-writer-" + std::to_string(next.fetch_add(1))); } while (!std::filesystem::create_directory(directory_)); p_ = directory_ / "result.h5"; } ~Temp() { std::error_code e; std::filesystem::remove_all(directory_, e); } const std::filesystem::path& Path() const { return p_; } private: std::filesystem::path directory_; std::filesystem::path p_; }; using fesa::hdf5_internal::Hdf5Handle; std::string Message(const fesa::Status& status) { return status.Diagnostics().empty() ? "" : status.Diagnostics()[0].message; } std::vector Bytes(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::binary); return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; } std::vector Diagnostics() { return {{fesa::Severity::kError, "later", {"b.inp", 9}, "HEAT TRANSFER", "part.0013", "late message"}, {fesa::Severity::kWarning, "early", {"a.inp", 2}, "NODE", "part.0007", u8"\uD55C\uAE00 warning"}}; } herr_t CollectAttribute(hid_t, const char* name, const H5A_info_t*, void* data) { static_cast*>(data)->push_back(name); return 0; } void CopyAttributes(hid_t from, hid_t to) { std::vector names; hsize_t at = 0; ASSERT_GE(H5Aiterate2(from, H5_INDEX_NAME, H5_ITER_INC, &at, CollectAttribute, &names), 0); for (const auto& name : names) { Hdf5Handle a{H5Aopen(from, name.c_str(), H5P_DEFAULT), H5Aclose}; Hdf5Handle t{H5Aget_type(a.Get()), H5Tclose}, s{H5Aget_space(a.Get()), H5Sclose}; std::vector value( static_cast(H5Sget_simple_extent_npoints(s.Get())) * H5Tget_size(t.Get())); ASSERT_GE(H5Aread(a.Get(), t.Get(), value.data()), 0); Hdf5Handle b{H5Acreate2(to, name.c_str(), t.Get(), s.Get(), H5P_DEFAULT, H5P_DEFAULT), H5Aclose}; ASSERT_GE(H5Awrite(b.Get(), t.Get(), value.data()), 0); if (H5Tis_variable_str(t.Get()) > 0 || H5Tdetect_class(t.Get(), H5T_VLEN) > 0) ASSERT_GE(H5Dvlen_reclaim(t.Get(), s.Get(), H5P_DEFAULT, value.data()), 0); } } void RewriteCompound(hid_t file, const char* path, const std::string& omit = "", const std::string& change = "", hid_t changed_type = -1, bool additive = false) { Hdf5Handle old{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose}; Hdf5Handle type{H5Dget_type(old.Get()), H5Tclose}, space{H5Dget_space(old.Get()), H5Sclose}; const int count = H5Tget_nmembers(type.Get()); ASSERT_GE(count, 0); std::vector names; std::vector fields; std::size_t width = 0; // Reverse field order and repack offsets; never preserve writer C++ padding. for (int i = count - 1; i >= 0; --i) { char* raw = H5Tget_member_name(type.Get(), static_cast(i)); ASSERT_NE(raw, nullptr); const std::string name{raw}; ASSERT_GE(H5free_memory(raw), 0); if (name == omit) continue; names.push_back(name); fields.emplace_back( name == change ? H5Tcopy(changed_type) : H5Tget_member_type(type.Get(), static_cast(i)), H5Tclose); width += H5Tget_size(fields.back().Get()); } if (additive) { names.push_back("optional_future_value"); fields.emplace_back(H5Tcopy(H5T_STD_U64LE), H5Tclose); width += 8; } Hdf5Handle replacement{H5Tcreate(H5T_COMPOUND, width), H5Tclose}; std::size_t offset = 0; for (std::size_t i = 0; i < names.size(); ++i) { ASSERT_GE( H5Tinsert(replacement.Get(), names[i].c_str(), offset, fields[i].Get()), 0); offset += H5Tget_size(fields[i].Get()); } const auto rows = static_cast(H5Sget_simple_extent_npoints(space.Get())); std::vector data(rows * width, 0); if (rows) { std::size_t field_offset = 0; for (std::size_t i = 0; i < names.size(); ++i) { if (names[i] != "optional_future_value") { Hdf5Handle projection{H5Tcreate(H5T_COMPOUND, width), H5Tclose}; ASSERT_GE(H5Tinsert(projection.Get(), names[i].c_str(), field_offset, fields[i].Get()), 0); const herr_t read = H5Dread(old.Get(), projection.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, data.data()); if (names[i] != change) ASSERT_GE(read, 0); } field_offset += H5Tget_size(fields[i].Get()); } } ASSERT_GE(H5Ldelete(file, path, H5P_DEFAULT), 0); Hdf5Handle properties{H5Pcreate(H5P_DATASET_CREATE), H5Pclose}; const hsize_t chunk = 1; ASSERT_GE(H5Pset_chunk(properties.Get(), 1, &chunk), 0); Hdf5Handle rewritten{H5Dcreate2(file, path, replacement.Get(), space.Get(), H5P_DEFAULT, properties.Get(), H5P_DEFAULT), H5Dclose}; ASSERT_GE(rewritten.Get(), 0); if (rows) ASSERT_GE(H5Dwrite(rewritten.Get(), replacement.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, data.data()), 0); CopyAttributes(old.Get(), rewritten.Get()); if (rows && H5Tdetect_class(replacement.Get(), H5T_VLEN) > 0) ASSERT_GE(H5Dvlen_reclaim(replacement.Get(), space.Get(), H5P_DEFAULT, data.data()), 0); else if (rows) { // H5Tdetect_class does not classify variable strings as H5T_VLEN. bool strings = false; for (const auto& field : fields) strings = strings || H5Tis_variable_str(field.Get()) > 0; if (strings) ASSERT_GE(H5Dvlen_reclaim(replacement.Get(), space.Get(), H5P_DEFAULT, data.data()), 0); } } template void WriteColumn(hid_t file, const char* path, const char* field, hid_t native, const std::vector& values) { Hdf5Handle d{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose}; Hdf5Handle t{H5Tcreate(H5T_COMPOUND, sizeof(T)), H5Tclose}; ASSERT_GE(H5Tinsert(t.Get(), field, 0, native), 0); ASSERT_GE( H5Dwrite(d.Get(), t.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0); } void StringAttribute(hid_t file, const char* path, const char* name, const std::string& value, H5T_cset_t charset = H5T_CSET_UTF8, bool scalar = true) { Hdf5Handle object{H5Oopen(file, path, H5P_DEFAULT), H5Oclose}; ASSERT_GE(H5Adelete(object.Get(), name), 0); Hdf5Handle t{H5Tcopy(H5T_C_S1), H5Tclose}; ASSERT_GE(H5Tset_size(t.Get(), value.size() + 1), 0); ASSERT_GE(H5Tset_cset(t.Get(), charset), 0); const hsize_t two = 2; Hdf5Handle s{ scalar ? H5Screate(H5S_SCALAR) : H5Screate_simple(1, &two, nullptr), H5Sclose}; Hdf5Handle a{H5Acreate2(object.Get(), name, t.Get(), s.Get(), H5P_DEFAULT, H5P_DEFAULT), H5Aclose}; std::vector data((value.size() + 1) * (scalar ? 1 : 2), 0); std::copy(value.begin(), value.end(), data.begin()); ASSERT_GE(H5Awrite(a.Get(), t.Get(), data.data()), 0); } void ReplacePrimitive(hid_t file, const std::string& path, bool wrong_shape) { Hdf5Handle old{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose}; Hdf5Handle old_type{H5Dget_type(old.Get()), H5Tclose}, old_space{H5Dget_space(old.Get()), H5Sclose}; const int rank = H5Sget_simple_extent_ndims(old_space.Get()); ASSERT_GT(rank, 0); std::vector shape(static_cast(rank)); ASSERT_GE(H5Sget_simple_extent_dims(old_space.Get(), shape.data(), nullptr), 0); if (wrong_shape) ++shape[0]; Hdf5Handle space{H5Screate_simple(rank, shape.data(), nullptr), H5Sclose}; ASSERT_GE(H5Ldelete(file, path.c_str(), H5P_DEFAULT), 0); Hdf5Handle replaced{ H5Dcreate2(file, path.c_str(), wrong_shape ? old_type.Get() : H5T_STD_I32LE, space.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), H5Dclose}; ASSERT_GE(replaced.Get(), 0); CopyAttributes(old.Get(), replaced.Get()); } void ChangePrimitive(hid_t file, const std::string& path) { Hdf5Handle d{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose}; Hdf5Handle t{H5Dget_type(d.Get()), H5Tclose}, s{H5Dget_space(d.Get()), H5Sclose}; const auto count = H5Sget_simple_extent_npoints(s.Get()); ASSERT_GT(count, 0); if (H5Tget_class(t.Get()) == H5T_FLOAT) { std::vector values(static_cast(count)); ASSERT_GE(H5Dread(d.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0); values[0] += 1; ASSERT_GE(H5Dwrite(d.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0); } else { std::vector values(static_cast(count)); ASSERT_GE(H5Dread(d.Get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0); values[0] += 1; ASSERT_GE(H5Dwrite(d.Get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0); } } TEST(ThermalHdf5ResultsWriter, WritesRawInitialAndCompletedZeroReactionHistory) { auto x = Make(); Temp out; fesa::ThermalHdf5ResultsWriter writer; const auto result = writer.Write( out.Path(), *x.domain, x.state, {{fesa::Severity::kWarning, "fixture", {}, "", "", "fixture"}}); ASSERT_TRUE(result.IsOk()) << (result.Diagnostics().empty() ? "no diagnostic" : result.Diagnostics()[0].message); hid_t file = H5Fopen(out.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); ASSERT_GE(file, 0); EXPECT_GT( H5Lexists(file, "/steps/Step-1/frames/0/nodal/temperature", H5P_DEFAULT), 0); EXPECT_LE(H5Lexists(file, "/steps/Step-1/frames/0/nodal/reaction_heat_rate", H5P_DEFAULT), 0); EXPECT_LE( H5Lexists(file, "/steps/Step-1/frames/0/element/heat_flux", H5P_DEFAULT), 0); EXPECT_GT(H5Lexists(file, "/steps/Step-1/frames/3/nodal/reaction_heat_rate", H5P_DEFAULT), 0); EXPECT_GT( H5Lexists(file, "/steps/Step-1/frames/3/element/heat_flux", H5P_DEFAULT), 0); EXPECT_GT(H5Fclose(file), -1); } TEST(ThermalHdf5ResultsWriter, SectionMaterialUsesGlobalMaterialIdentity) { auto x = Make(); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); using fesa::hdf5_internal::Hdf5Handle; Hdf5Handle f{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; Hdf5Handle d{H5Dopen2(f.Get(), "/model/shell_sections", H5P_DEFAULT), H5Dclose}; Hdf5Handle t{H5Tcreate(H5T_COMPOUND, sizeof(std::uint64_t)), H5Tclose}; ASSERT_GE(H5Tinsert(t.Get(), "material_internal_id", 0, H5T_NATIVE_UINT64), 0); std::uint64_t id = 99; ASSERT_GE(H5Dread(d.Get(), t.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, &id), 0); EXPECT_EQ(id, 1U); Hdf5Handle utf8{H5Tcopy(H5T_C_S1), H5Tclose}; ASSERT_GE(H5Tset_size(utf8.Get(), H5T_VARIABLE), 0); ASSERT_GE(H5Tset_cset(utf8.Get(), H5T_CSET_UTF8), 0); Hdf5Handle source_type{H5Tcreate(H5T_COMPOUND, sizeof(char*)), H5Tclose}; ASSERT_GE(H5Tinsert(source_type.Get(), "source_file", 0, utf8.Get()), 0); char* source = nullptr; ASSERT_GE(H5Dread(d.Get(), source_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, &source), 0); ASSERT_NE(source, nullptr); EXPECT_EQ(std::string{source}, std::filesystem::absolute("thermal-writer.inp") .lexically_normal() .generic_u8string()); EXPECT_GE(H5free_memory(source), 0); } TEST(ThermalHdf5ResultsWriter, RejectsMissingMandatoryAttributesAndChangedValues) { const auto x = Make(); for (const auto& mutation : std::vector>{ [](hid_t f) { hid_t m = H5Gopen2(f, "/metadata", H5P_DEFAULT); EXPECT_GE(H5Adelete(m, "source_input_identity"), 0); H5Gclose(m); }, [](hid_t f) { hid_t d = H5Dopen2(f, "/steps/Step-1/frames/1/nodal/temperature", H5P_DEFAULT); EXPECT_GE(H5Adelete(d, "units_label"), 0); H5Dclose(d); }, [](hid_t f) { hid_t d = H5Dopen2(f, "/model/thermal_node_ids", H5P_DEFAULT); std::uint64_t ids[4] = {1, 1, 3, 4}; EXPECT_GE(H5Dwrite(d, H5T_NATIVE_UINT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, ids), 0); H5Dclose(d); }, [](hid_t f) { hid_t d = H5Dopen2(f, "/steps/Step-1/frames/1/nodal/temperature", H5P_DEFAULT); double v[20] = {4}; EXPECT_GE(H5Dwrite(d, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, v), 0); H5Dclose(d); }}) { Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); hid_t f = H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT); ASSERT_GE(f, 0); mutation(f); ASSERT_GE(H5Fclose(f), 0); EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, {}) .IsOk()); } } TEST(ThermalHdf5ResultsWriter, EveryRequiredAttributeIsCheckedBeforePublication) { const auto x = Make(); std::vector> required; const auto add = [&](const std::string& path, std::initializer_list names) { for (const auto* name : names) required.emplace_back(path, name); }; add("/metadata", {"schema_version", "feature_id", "analysis_procedure", "source_step_name", "solver_version", "source_input_identity", "unit_system_label", "internal_formulation", "integration_rule", "time_integration", "nominal_time_increment", "time_period"}); add("/model/nodes", {"units_label"}); add("/model/shell_sections", {"thickness_units_label"}); add("/model/thermal_materials", {"conductivity_units_label", "density_units_label", "specific_heat_units_label"}); add("/steps/Step-1/accepted_frames", {"time_units_label"}); for (int frame = 0; frame < 4; ++frame) { const auto path = "/steps/Step-1/frames/" + std::to_string(frame); add(path, {"frame_index", "increment_index", "time", "time_increment", "is_initial"}); add(path + "/nodal/temperature", {"component_order", "units_label", "coordinate_system", "location"}); if (frame != 0) { add(path + "/nodal/reaction_heat_rate", {"component_order", "units_label", "coordinate_system", "location", "positive_direction"}); add(path + "/element/heat_flux", {"component_order", "units_label", "coordinate_system", "location"}); } } for (const auto& attribute : required) { SCOPED_TRACE(attribute.first + ":" + attribute.second); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); const hid_t file = H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT); ASSERT_GE(file, 0); const hid_t object = H5Oopen(file, attribute.first.c_str(), H5P_DEFAULT); ASSERT_GE(object, 0); ASSERT_GE(H5Adelete(object, attribute.second.c_str()), 0); ASSERT_GE(H5Oclose(object), 0); ASSERT_GE(H5Fclose(file), 0); EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, {}) .IsOk()); } } TEST(ThermalHdf5ResultsWriter, EveryCompoundFieldAndEveryDatasetIsMandatory) { const auto x = Make(); const auto diagnostics = Diagnostics(); const std::vector>> tables = { {"/model/nodes", {"internal_node_id", "instance_name", "source_label", "coordinates"}}, {"/model/elements", {"internal_element_id", "instance_name", "source_label", "source_element_type", "internal_formulation", "node_internal_ids", "shell_section_internal_id", "material_internal_id"}}, {"/model/thermal_materials", {"internal_material_id", "name", "conductivity", "density", "specific_heat"}}, {"/model/shell_sections", {"internal_section_id", "source_file", "source_line", "source_elset", "material_internal_id", "thickness"}}, {"/model/thermal_reaction_dofs", {"internal_node_id", "source_dof"}}, {"/steps/Step-1/accepted_frames", {"frame_index", "increment_index", "time", "time_increment", "is_initial"}}, {"/diagnostics", {"severity", "code", "file", "line", "keyword", "entity_identity", "message"}}}; for (const auto& table : tables) for (const auto* field : table.second) { SCOPED_TRACE(std::string{table.first} + ":" + field); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, diagnostics) .IsOk()); { Hdf5Handle file{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; RewriteCompound(file.Get(), table.first, field); } const auto checked = fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, diagnostics); EXPECT_FALSE(checked.IsOk()); EXPECT_NE(Message(checked).find("Missing compound field"), std::string::npos); } std::vector datasets; for (const auto& table : tables) datasets.push_back(table.first); for (const auto* name : {"thermal_node_ids", "thermal_element_ids", "temperature_dofs", "temperature_zeta", "thermal_constraint_mask", "thermal_flux_inplane_coordinates", "thermal_flux_zeta", "thermal_flux_trace_side", "thermal_flux_local_frames"}) datasets.push_back(std::string{"/model/"} + name); for (std::size_t i = 0; i < x.state.AcceptedFrames().size(); ++i) { const auto root = "/steps/Step-1/frames/" + std::to_string(i); datasets.push_back(root + "/nodal/temperature"); if (i) { datasets.push_back(root + "/nodal/reaction_heat_rate"); datasets.push_back(root + "/element/heat_flux"); } } for (const auto& path : datasets) { SCOPED_TRACE(path); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, diagnostics) .IsOk()); { Hdf5Handle file{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; ASSERT_GE(H5Ldelete(file.Get(), path.c_str(), H5P_DEFAULT), 0); } EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, diagnostics) .IsOk()); } } TEST(ThermalHdf5ResultsWriter, ReorderedPackedAdditiveCompoundsAndFixedUtf8AreAccepted) { auto x = Make(); const auto diagnostics = Diagnostics(); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, diagnostics) .IsOk()); { Hdf5Handle file{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; for (const auto* path : {"/model/nodes", "/model/elements", "/model/thermal_materials", "/model/shell_sections", "/model/thermal_reaction_dofs", "/steps/Step-1/accepted_frames", "/diagnostics"}) RewriteCompound(file.Get(), path, "", "", -1, true); StringAttribute(file.Get(), "/metadata", "source_step_name", "raw Step"); StringAttribute(file.Get(), "/steps/Step-1/frames/0/nodal/temperature", "units_label", "temperature"); Hdf5Handle fixed{H5Tcopy(H5T_C_S1), H5Tclose}; ASSERT_GE(H5Tset_size(fixed.Get(), 16), 0); ASSERT_GE(H5Tset_cset(fixed.Get(), H5T_CSET_UTF8), 0); { fesa::hdf5_internal::Hdf5ErrorSilencer silent; RewriteCompound(file.Get(), "/model/thermal_materials", "", "name", fixed.Get()); } std::array name{}; std::copy_n("thermal", 7, name.data()); WriteColumn(file.Get(), "/model/thermal_materials", "name", fixed.Get(), std::vector>{name}); } const auto checked = fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, diagnostics); EXPECT_TRUE(checked.IsOk()) << Message(checked); } TEST(ThermalHdf5ResultsWriter, RejectsWrongTypesArrayExtentsNonscalarAndNonUtf8Attributes) { auto x = Make(); const hsize_t wrong_extent = 3; Hdf5Handle array{H5Tarray_create2(H5T_STD_U64LE, 1, &wrong_extent), H5Tclose}; const std::vector> mutations = { [](hid_t f) { RewriteCompound(f, "/model/thermal_materials", "", "conductivity", H5T_IEEE_F32LE); }, [](hid_t f) { RewriteCompound(f, "/model/elements", "", "internal_element_id", H5T_STD_U32LE); }, [&](hid_t f) { RewriteCompound(f, "/model/elements", "", "node_internal_ids", array.Get()); }, [](hid_t f) { StringAttribute(f, "/metadata", "feature_id", "shell-transient-heat-transfer", H5T_CSET_ASCII); }, [](hid_t f) { StringAttribute(f, "/metadata", "source_step_name", "raw Step", H5T_CSET_UTF8, false); }, [](hid_t f) { Hdf5Handle g{H5Gopen2(f, "/steps/Step-1/frames/1", H5P_DEFAULT), H5Gclose}; ASSERT_GE(H5Adelete(g.Get(), "is_initial"), 0); const hsize_t count = 2; Hdf5Handle s{H5Screate_simple(1, &count, nullptr), H5Sclose}; Hdf5Handle a{H5Acreate2(g.Get(), "is_initial", H5T_STD_U8LE, s.Get(), H5P_DEFAULT, H5P_DEFAULT), H5Aclose}; const std::uint8_t data[2] = {0, 0}; ASSERT_GE(H5Awrite(a.Get(), H5T_NATIVE_UINT8, data), 0); }, [](hid_t f) { Hdf5Handle g{H5Gopen2(f, "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(H5Adelete(g.Get(), "time_period"), 0); const hsize_t count = 2; Hdf5Handle s{H5Screate_simple(1, &count, nullptr), H5Sclose}; Hdf5Handle a{H5Acreate2(g.Get(), "time_period", H5T_IEEE_F64LE, s.Get(), H5P_DEFAULT, H5P_DEFAULT), H5Aclose}; const double data[2] = {.7, .7}; ASSERT_GE(H5Awrite(a.Get(), H5T_NATIVE_DOUBLE, data), 0); }}; for (std::size_t i = 0; i < mutations.size(); ++i) { SCOPED_TRACE(i); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); { fesa::hdf5_internal::Hdf5ErrorSilencer silent; Hdf5Handle file{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; mutations[i](file.Get()); } const auto checked = fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, {}); EXPECT_FALSE(checked.IsOk()); if (i < 3) EXPECT_NE(Message(checked).find("type/extent"), std::string::npos) << Message(checked); } } TEST(ThermalHdf5ResultsWriter, RejectsWrongValuesFrameInventoriesNonfiniteAndDiagnostics) { auto definition = Definition(); definition.thermal_model_boundaries = {{1, 11, 5, {}}, {4, 15, -4, {}}}; const auto x = Make(std::move(definition)); const auto diagnostics = Diagnostics(); std::vector> mutations = { [](hid_t f) { WriteColumn(f, "/model/nodes", "internal_node_id", H5T_NATIVE_UINT64, std::vector{0, 1, 1, 3, 4}); }, [](hid_t f) { WriteColumn(f, "/model/elements", "shell_section_internal_id", H5T_NATIVE_UINT64, std::vector{77}); }, [](hid_t f) { WriteColumn(f, "/model/thermal_materials", "conductivity", H5T_NATIVE_DOUBLE, std::vector{3}); }, [](hid_t f) { WriteColumn(f, "/model/shell_sections", "source_line", H5T_NATIVE_UINT64, std::vector{99}); }, [](hid_t f) { WriteColumn(f, "/model/thermal_reaction_dofs", "source_dof", H5T_NATIVE_UINT64, std::vector{12, 15}); }, [](hid_t f) { WriteColumn(f, "/steps/Step-1/accepted_frames", "frame_index", H5T_NATIVE_UINT64, std::vector{0, 1, 1, 3}); }, [](hid_t f) { WriteColumn(f, "/steps/Step-1/accepted_frames", "time_increment", H5T_NATIVE_DOUBLE, std::vector{0, .3, .3, .3}); }, [](hid_t f) { WriteColumn(f, "/diagnostics", "line", H5T_NATIVE_UINT64, std::vector{9, 2}); }, [](hid_t f) { Hdf5Handle g{H5Gcreate2(f, "/steps/Step-1/frames/99", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), H5Gclose}; ASSERT_GE(g.Get(), 0); }, [](hid_t f) { ASSERT_GE(H5Ldelete(f, "/steps/Step-1/frames/2", H5P_DEFAULT), 0); }, [](hid_t f) { ASSERT_GE( H5Lcreate_soft("/missing", f, "/steps/Step-1/frames/0/element", H5P_DEFAULT, H5P_DEFAULT), 0); }, [](hid_t f) { Hdf5Handle d{ H5Dopen2(f, "/steps/Step-1/frames/1/nodal/reaction_heat_rate", H5P_DEFAULT), H5Dclose}; const double values[] = {std::numeric_limits::quiet_NaN(), -9}; ASSERT_GE(H5Dwrite(d.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values), 0); }, [](hid_t f) { StringAttribute(f, "/metadata", "source_input_identity", "wrong-input-bytes"); }}; for (std::size_t i = 0; i < mutations.size(); ++i) { SCOPED_TRACE(i); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, diagnostics) .IsOk()); { Hdf5Handle file{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; mutations[i](file.Get()); } EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, diagnostics) .IsOk()); } } TEST(ThermalHdf5ResultsWriter, RotatedFramesAndEndpointCloseScheduleRoundTripIndependently) { auto definition = Definition(); definition.thermal_step->time_period = .9; // Positive 90-degree rotation about X: columns // e1=(1,0,0),e2=(0,0,1),n=(0,-1,0). for (std::size_t i = 1; i < 5; ++i) { const double y = definition.nodes[i].coordinates[1]; definition.nodes[i].coordinates[1] = 0; definition.nodes[i].coordinates[2] = y; } const auto x = Make(std::move(definition)); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); Hdf5Handle f{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; Hdf5Handle d{ H5Dopen2(f.Get(), "/model/thermal_flux_local_frames", H5P_DEFAULT), H5Dclose}; std::vector frames(36); ASSERT_GE(H5Dread(d.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, frames.data()), 0); const double expected[] = {1, 0, 0, 0, 0, -1, 0, 1, 0}; for (std::size_t i = 0; i < frames.size(); ++i) EXPECT_DOUBLE_EQ(frames[i], expected[i % 9]); Hdf5Handle g{H5Gopen2(f.Get(), "/steps/Step-1/frames/3", H5P_DEFAULT), H5Gclose}; Hdf5Handle a{H5Aopen(g.Get(), "time_increment", H5P_DEFAULT), H5Aclose}; double actual = 0; ASSERT_GE(H5Aread(a.Get(), H5T_NATIVE_DOUBLE, &actual), 0); EXPECT_EQ(actual, .3); EXPECT_NE(actual, .9 - .6); Hdf5Handle initial{ H5Dopen2(f.Get(), "/steps/Step-1/frames/0/nodal/temperature", H5P_DEFAULT), H5Dclose}; double t[20]; ASSERT_GE(H5Dread(initial.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, t), 0); for (std::size_t i = 0; i < 20; ++i) EXPECT_EQ(t[i], i < 5 ? -2 : 0); Hdf5Handle flux{H5Dopen2(f.Get(), "/steps/Step-1/frames/3/element/heat_flux", H5P_DEFAULT), H5Dclose}; double q[72]; ASSERT_GE( H5Dread(flux.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, q), 0); for (std::size_t point = 0; point < 4; ++point) for (std::size_t trace = 0; trace < 6; ++trace) { const auto sample = point * 6 + trace; EXPECT_EQ(q[3 * sample], static_cast(sample)); EXPECT_EQ(q[3 * sample + 1], -static_cast(sample) - 3); EXPECT_EQ(q[3 * sample + 2], 9); } } herr_t FailedClose(hid_t file) { const auto closed = H5Fclose(file); EXPECT_GE(closed, 0); return -1; } TEST(ThermalHdf5ResultsWriter, EveryFailedLifecycleBoundaryPreservesOldFinalAndCleansCandidate) { const auto x = Make(); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); const auto previous = Bytes(out.Path()); std::vector failures(5); failures[0].before_write = [](hid_t file) { const hsize_t size = 1; Hdf5Handle s{H5Screate_simple(1, &size, nullptr), H5Sclose}; Hdf5Handle d{H5Dcreate2(file, "/metadata", H5T_STD_U8LE, s.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), H5Dclose}; ASSERT_GE(d.Get(), 0); }; failures[1].candidate_close = FailedClose; failures[2].before_self_check = [](const auto& candidate) { Hdf5Handle f{H5Fopen(candidate.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; Hdf5Handle m{H5Gopen2(f.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(H5Adelete(m.Get(), "schema_version"), 0); }; failures[3].reader_close = FailedClose; failures[4].before_finalize = [](const auto& candidate) { EXPECT_TRUE(std::filesystem::remove(candidate)); }; for (std::size_t i = 0; i < failures.size(); ++i) { SCOPED_TRACE(i); EXPECT_FALSE(fesa::hdf5_internal::WriteThermalFile(out.Path(), *x.domain, x.state, {}, failures[i]) .IsOk()); EXPECT_EQ(Bytes(out.Path()), previous); std::size_t files = 0; for (const auto& entry : std::filesystem::directory_iterator(out.Path().parent_path())) { ++files; EXPECT_EQ(entry.path(), out.Path()); } EXPECT_EQ(files, 1U); } } TEST(ThermalHdf5ResultsWriter, DomainMismatchAndIncompleteHistoryCannotReplaceFinal) { const auto x = Make(); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); const auto previous = Bytes(out.Path()); auto definition = Definition(); definition.nodes[1].source_id = {"part", 8, "0008"}; auto different = fesa::Domain::Create(std::move(definition)); ASSERT_TRUE(different.HasValue()); EXPECT_FALSE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), different.Value(), x.state, {}) .IsOk()); const auto partial = Make(Definition(), false); EXPECT_FALSE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *partial.domain, partial.state, {}) .IsOk()); EXPECT_EQ(Bytes(out.Path()), previous); auto constrained = Definition(); constrained.thermal_model_boundaries = {{1, 11, 5, {}}}; const auto prescribed = Make(constrained); constrained.thermal_model_boundaries[0].value = 6; auto changed_g = fesa::Domain::Create(std::move(constrained)); ASSERT_TRUE(changed_g.HasValue()); EXPECT_FALSE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), changed_g.Value(), prescribed.state, {}) .IsOk()); EXPECT_EQ(Bytes(out.Path()), previous); } TEST(ThermalHdf5ResultsWriter, EveryPrimitiveDatasetChecksShapeTypeAndValue) { auto definition = Definition(); definition.thermal_model_boundaries = {{1, 11, 5, {}}}; const auto x = Make(std::move(definition)); std::vector datasets; for (const auto* name : {"thermal_node_ids", "thermal_element_ids", "temperature_dofs", "temperature_zeta", "thermal_constraint_mask", "thermal_flux_inplane_coordinates", "thermal_flux_zeta", "thermal_flux_trace_side", "thermal_flux_local_frames"}) datasets.push_back(std::string{"/model/"} + name); for (std::size_t i = 0; i < x.state.AcceptedFrames().size(); ++i) { const auto root = "/steps/Step-1/frames/" + std::to_string(i); datasets.push_back(root + "/nodal/temperature"); if (i) { datasets.push_back(root + "/nodal/reaction_heat_rate"); datasets.push_back(root + "/element/heat_flux"); } } for (const auto& path : datasets) for (int mutation = 0; mutation < 3; ++mutation) { SCOPED_TRACE(path + ":" + std::to_string(mutation)); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); { Hdf5Handle f{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; if (mutation == 2) ChangePrimitive(f.Get(), path); else ReplacePrimitive(f.Get(), path, mutation == 0); } const auto checked = fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, {}); EXPECT_FALSE(checked.IsOk()); if (mutation == 0) EXPECT_NE(Message(checked).find("extent"), std::string::npos) << Message(checked); if (mutation == 1) EXPECT_NE(Message(checked).find("primitive type"), std::string::npos) << Message(checked); } for (const auto* path : {"/model/nodes", "/model/elements", "/model/thermal_materials", "/model/shell_sections", "/model/thermal_reaction_dofs", "/steps/Step-1/accepted_frames", "/diagnostics"}) { SCOPED_TRACE(path); Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); { Hdf5Handle f{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; ReplacePrimitive(f.Get(), path, true); } const auto checked = fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, {}); EXPECT_FALSE(checked.IsOk()); EXPECT_NE(Message(checked).find("extent"), std::string::npos) << Message(checked); } } TEST(ThermalHdf5ResultsWriter, InitialRecoveryCannotBeFilledWithCompletedZeros) { const auto x = Make(); for (const bool flux : {false, true}) { Temp out; ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{} .Write(out.Path(), *x.domain, x.state, {}) .IsOk()); { Hdf5Handle f{ H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; if (flux) { Hdf5Handle g{H5Gcreate2(f.Get(), "/steps/Step-1/frames/0/element", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), H5Gclose}; ASSERT_GE(g.Get(), 0); ASSERT_GE(H5Ocopy(f.Get(), "/steps/Step-1/frames/1/element/heat_flux", f.Get(), "/steps/Step-1/frames/0/element/heat_flux", H5P_DEFAULT, H5P_DEFAULT), 0); } else ASSERT_GE( H5Ocopy(f.Get(), "/steps/Step-1/frames/1/nodal/reaction_heat_rate", f.Get(), "/steps/Step-1/frames/0/nodal/reaction_heat_rate", H5P_DEFAULT, H5P_DEFAULT), 0); } EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile( out.Path(), *x.domain, x.state, {}) .IsOk()); } } } // namespace