From 3329b41d12116c8fcbac125beb6f4af01c436fe1 Mon Sep 17 00:00:00 2001 From: GuySten Date: Mon, 6 Oct 2025 00:33:43 +0300 Subject: [PATCH 1/9] load mesh objects from HDF5 file --- include/openmc/mesh.h | 17 +++ include/openmc/weight_windows.h | 5 +- src/mesh.cpp | 246 ++++++++++++++++++++++++++++++++ src/weight_windows.cpp | 14 +- 4 files changed, 277 insertions(+), 5 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index a56705c9ec5..a327f8bb335 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -132,6 +132,7 @@ class Mesh { // Constructors and destructor Mesh() = default; Mesh(pugi::xml_node node); + Mesh(hid_t group); virtual ~Mesh() = default; // Methods @@ -258,6 +259,7 @@ class StructuredMesh : public Mesh { public: StructuredMesh() = default; StructuredMesh(pugi::xml_node node) : Mesh {node} {}; + StructuredMesh(hid_t group) : Mesh {group} {}; virtual ~StructuredMesh() = default; using MeshIndex = std::array; @@ -423,6 +425,7 @@ class PeriodicStructuredMesh : public StructuredMesh { public: PeriodicStructuredMesh() = default; PeriodicStructuredMesh(pugi::xml_node node) : StructuredMesh {node} {}; + PeriodicStructuredMesh(hid_t group) : StructuredMesh {group} {}; Position local_coords(const Position& r) const override { @@ -442,6 +445,7 @@ class RegularMesh : public StructuredMesh { // Constructors RegularMesh() = default; RegularMesh(pugi::xml_node node); + RegularMesh(hid_t group); // Overridden methods int get_index_in_direction(double r, int i) const override; @@ -492,6 +496,7 @@ class RectilinearMesh : public StructuredMesh { // Constructors RectilinearMesh() = default; RectilinearMesh(pugi::xml_node node); + RectilinearMesh(hid_t group); // Overridden methods int get_index_in_direction(double r, int i) const override; @@ -534,6 +539,7 @@ class CylindricalMesh : public PeriodicStructuredMesh { // Constructors CylindricalMesh() = default; CylindricalMesh(pugi::xml_node node); + CylindricalMesh(hid_t group); // Overridden methods virtual MeshIndex get_indices(Position r, bool& in_mesh) const override; @@ -598,6 +604,7 @@ class SphericalMesh : public PeriodicStructuredMesh { // Constructors SphericalMesh() = default; SphericalMesh(pugi::xml_node node); + SphericalMesh(hid_t group); // Overridden methods virtual MeshIndex get_indices(Position r, bool& in_mesh) const override; @@ -668,6 +675,7 @@ class UnstructuredMesh : public Mesh { // Constructors UnstructuredMesh() { n_dimension_ = 3; }; UnstructuredMesh(pugi::xml_node node); + UnstructuredMesh(hid_t group); static const std::string mesh_type; virtual std::string get_mesh_type() const override; @@ -774,6 +782,7 @@ class MOABMesh : public UnstructuredMesh { // Constructors MOABMesh() = default; MOABMesh(pugi::xml_node); + MOABMesh(hid_t group); MOABMesh(const std::string& filename, double length_multiplier = 1.0); MOABMesh(std::shared_ptr external_mbi); @@ -943,6 +952,7 @@ class LibMesh : public UnstructuredMesh { public: // Constructors LibMesh(pugi::xml_node node); + LibMesh(hid_t group); LibMesh(const std::string& filename, double length_multiplier = 1.0); LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier = 1.0); @@ -1069,6 +1079,13 @@ class AdaptiveLibMesh : public LibMesh { //! \param[in] root XML node void read_meshes(pugi::xml_node root); +//! Read meshes from a HDF5 file +// +//! \param[in] group HDF5 group +//! \param[out] mesh_map mapping from mesh ids inside HDF5 group to global mesh +//! ids +void read_meshes(hid_t group, std::unordered_map& mesh_map); + //! Write mesh data to an HDF5 group // //! \param[in] group HDF5 group diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 7638155228e..25421ef4957 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -87,8 +87,9 @@ class WeightWindows { WeightWindows(pugi::xml_node node); ~WeightWindows(); static WeightWindows* create(int32_t id = -1); - static WeightWindows* from_hdf5( - hid_t wws_group, const std::string& group_name); + static WeightWindows* from_hdf5(hid_t wws_group, + const std::string& group_name, + std::unordered_map& id_map); //---------------------------------------------------------------------------- // Methods diff --git a/src/mesh.cpp b/src/mesh.cpp index 58d218b9cad..94b75f6eda8 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -238,6 +238,17 @@ Mesh::Mesh(pugi::xml_node node) name_ = get_node_value(node, "name"); } +Mesh::Mesh(hid_t group) +{ + // Read mesh ID + read_attribute(group, "id", id_); + + // Read mesh name + if (object_exists(group, "name")) { + read_dataset(group, "name", name_); + } +} + void Mesh::set_id(int32_t id) { assert(id >= 0 || id == C_NONE); @@ -627,6 +638,46 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) } } +UnstructuredMesh::UnstructuredMesh(hid_t group) : Mesh(group) +{ + n_dimension_ = 3; + + // check the mesh type + if (object_exists(group, "type")) { + std::string temp; + read_dataset(group, "type", temp); + if (temp != mesh_type) { + fatal_error(fmt::format("Invalid mesh type: {}", temp)); + } + } + + // check if a length unit multiplier was specified + if (object_exists(group, "length_multiplier")) { + read_dataset(group, "length_multiplier", length_multiplier_); + } + + // get the filename of the unstructured mesh to load + if (object_exists(group, "filename")) { + read_dataset(group, "filename", filename_); + if (!file_exists(filename_)) { + fatal_error("Mesh file '" + filename_ + "' does not exist!"); + } + } else { + fatal_error(fmt::format( + "No filename supplied for unstructured mesh with ID: {}", id_)); + } + + if (attribute_exists(group, "options")) { + read_attribute(group, "options", options_); + } + + // check if mesh tally data should be written with + // statepoint files + if (attribute_exists(group, "output")) { + read_attribute(group, "output", output_); + } +} + void UnstructuredMesh::determine_bounds() { double xmin = INFTY; @@ -1174,6 +1225,73 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} } } +RegularMesh::RegularMesh(hid_t group) : StructuredMesh {group} +{ + // Determine number of dimensions for mesh + if (!object_exists(group, "dimension")) { + fatal_error("Must specify on a regular mesh."); + } + + xt::xtensor shape; + read_dataset(group, "dimension", shape); + int n = n_dimension_ = shape.size(); + if (n != 1 && n != 2 && n != 3) { + fatal_error("Mesh must be one, two, or three dimensions."); + } + std::copy(shape.begin(), shape.end(), shape_.begin()); + + // Check that dimensions are all greater than zero + if (xt::any(shape <= 0)) { + fatal_error("All entries on the element for a tally " + "mesh must be positive."); + } + + // Check for lower-left coordinates + if (object_exists(group, "lower_left")) { + // Read mesh lower-left corner location + read_dataset(group, "lower_left", lower_left_); + } else { + fatal_error("Must specify on a mesh."); + } + + // Make sure lower_left and dimension match + if (shape.size() != lower_left_.size()) { + fatal_error("Number of entries on must be the same " + "as the number of entries on ."); + } + + if (object_exists(group, "upper_right")) { + + read_dataset(group, "upper_right", upper_right_); + + // Check to ensure width has same dimensions + auto n = upper_right_.size(); + if (n != lower_left_.size()) { + fatal_error("Number of entries on must be the " + "same as the number of entries on ."); + } + + // Check that upper-right is above lower-left + if (xt::any(upper_right_ < lower_left_)) { + fatal_error("The coordinates must be greater than " + "the coordinates on a tally mesh."); + } + + // Set width + width_ = xt::eval((upper_right_ - lower_left_) / shape); + } else { + fatal_error("Must specify either or on a mesh."); + } + + // Set material volumes + volume_frac_ = 1.0 / xt::prod(shape)(); + + element_volume_ = 1.0; + for (int i = 0; i < n_dimension_; i++) { + element_volume_ *= width_[i]; + } +} + int RegularMesh::get_index_in_direction(double r, int i) const { return std::ceil((r - lower_left_[i]) / width_[i]); @@ -1343,6 +1461,19 @@ RectilinearMesh::RectilinearMesh(pugi::xml_node node) : StructuredMesh {node} } } +RectilinearMesh::RectilinearMesh(hid_t group) : StructuredMesh {group} +{ + n_dimension_ = 3; + + read_dataset(group, "x_grid", grid_[0]); + read_dataset(group, "y_grid", grid_[1]); + read_dataset(group, "z_grid", grid_[2]); + + if (int err = set_grid()) { + fatal_error(openmc_err_msg); + } +} + const std::string RectilinearMesh::mesh_type = "rectilinear"; std::string RectilinearMesh::get_mesh_type() const @@ -1478,6 +1609,19 @@ CylindricalMesh::CylindricalMesh(pugi::xml_node node) } } +CylindricalMesh::CylindricalMesh(hid_t group) : PeriodicStructuredMesh {group} +{ + n_dimension_ = 3; + read_dataset(group, "r_grid", grid_[0]); + read_dataset(group, "phi_grid", grid_[1]); + read_dataset(group, "z_grid", grid_[2]); + read_dataset(group, "origin", origin_); + + if (int err = set_grid()) { + fatal_error(openmc_err_msg); + } +} + const std::string CylindricalMesh::mesh_type = "cylindrical"; std::string CylindricalMesh::get_mesh_type() const @@ -1756,6 +1900,20 @@ SphericalMesh::SphericalMesh(pugi::xml_node node) } } +SphericalMesh::SphericalMesh(hid_t group) : PeriodicStructuredMesh {group} +{ + n_dimension_ = 3; + + read_dataset(group, "r_grid", grid_[0]); + read_dataset(group, "theta_grid", grid_[1]); + read_dataset(group, "phi_grid", grid_[2]); + read_dataset(group, "origin", origin_); + + if (int err = set_grid()) { + fatal_error(openmc_err_msg); + } +} + const std::string SphericalMesh::mesh_type = "spherical"; std::string SphericalMesh::get_mesh_type() const @@ -2520,6 +2678,11 @@ MOABMesh::MOABMesh(pugi::xml_node node) : UnstructuredMesh(node) initialize(); } +MOABMesh::MOABMesh(hid_t group) : UnstructuredMesh(group) +{ + initialize(); +} + MOABMesh::MOABMesh(const std::string& filename, double length_multiplier) : UnstructuredMesh() { @@ -3228,6 +3391,15 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) initialize(); } +LibMesh::LibMesh(hid_t group) : UnstructuredMesh(group) +{ + // filename_ and length_multiplier_ will already be set by the + // UnstructuredMesh constructor + set_mesh_pointer_from_filename(filename_); + set_length_multiplier(length_multiplier_); + initialize(); +} + // create the mesh from a pointer to a libMesh Mesh LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) { @@ -3649,6 +3821,80 @@ void read_meshes(pugi::xml_node root) } } +void read_meshes(hid_t group, std::unordered_map& mesh_map) +{ + // Find the iterator to the element with the maximum key + auto max_key_it = + std::max_element(model::mesh_map.begin(), model::mesh_map.end(), + [](const auto& p1, const auto& p2) { return p1.first < p2.first; }); + auto offset = max_key_it->first; + + std::unordered_set mesh_ids; + + std::array ids; + read_attribute(group, "ids", ids); + + for (auto id : ids) { + + mesh_map[id] = id + offset; + + // Check to make sure multiple meshes in the same file don't share IDs + if (contains(mesh_ids, id)) { + fatal_error(fmt::format("Two or more meshes use the same unique ID " + "'{}' in the same HDF5 input file", + id)); + } + mesh_ids.insert(id); + + std::string name = fmt::format("mesh {}", id); + hid_t mesh_group = open_group(group, name.c_str()); + + std::string mesh_type; + if (attribute_exists(mesh_group, "type")) { + read_dataset(mesh_group, "type", mesh_type); + } else { + mesh_type = "regular"; + } + + // determine the mesh library to use + std::string mesh_lib; + if (attribute_exists(mesh_group, "library")) { + read_dataset(mesh_group, "library", mesh_lib); + } + + // Read mesh and add to vector + if (mesh_type == RegularMesh::mesh_type) { + model::meshes.push_back(make_unique(mesh_group)); + } else if (mesh_type == RectilinearMesh::mesh_type) { + model::meshes.push_back(make_unique(mesh_group)); + } else if (mesh_type == CylindricalMesh::mesh_type) { + model::meshes.push_back(make_unique(mesh_group)); + } else if (mesh_type == SphericalMesh::mesh_type) { + model::meshes.push_back(make_unique(mesh_group)); +#ifdef OPENMC_DAGMC_ENABLED + } else if (mesh_type == UnstructuredMesh::mesh_type && + mesh_lib == MOABMesh::mesh_lib_type) { + model::meshes.push_back(make_unique(mesh_group)); +#endif +#ifdef OPENMC_LIBMESH_ENABLED + } else if (mesh_type == UnstructuredMesh::mesh_type && + mesh_lib == LibMesh::mesh_lib_type) { + model::meshes.push_back(make_unique(mesh_group)); +#endif + } else if (mesh_type == UnstructuredMesh::mesh_type) { + fatal_error("Unstructured mesh support is not enabled or the mesh " + "library is invalid."); + } else { + fatal_error("Invalid mesh type: " + mesh_type); + } + + model::meshes.back()->id_ = mesh_map[model::meshes.back()->id_]; + + // Map ID to position in vector + model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1; + } +} + void meshes_to_hdf5(hid_t group) { // Write number of meshes diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 26762ad18a6..6084a02753b 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -241,8 +241,8 @@ WeightWindows* WeightWindows::create(int32_t id) return wws; } -WeightWindows* WeightWindows::from_hdf5( - hid_t wws_group, const std::string& group_name) +WeightWindows* WeightWindows::from_hdf5(hid_t wws_group, + const std::string& group_name, std::unordered_map& id_map) { // collect ID from the name of this group hid_t ww_group = open_group(wws_group, group_name); @@ -258,6 +258,8 @@ WeightWindows* WeightWindows::from_hdf5( int32_t mesh_id; read_dataset(ww_group, "mesh", mesh_id); + mesh_id = id_map[mesh_id]; + if (model::mesh_map.count(mesh_id) == 0) { fatal_error( fmt::format("Mesh {} used in weight windows does not exist.", mesh_id)); @@ -1328,10 +1330,16 @@ extern "C" int openmc_weight_windows_import(const char* filename) hid_t weight_windows_group = open_group(ww_file, "weight_windows"); + std::unordered_map mesh_map; + + hid_t mesh_group = open_group(ww_file, "meshes"); + + read_meshes(mesh_group, mesh_map); + std::vector names = group_names(weight_windows_group); for (const auto& name : names) { - WeightWindows::from_hdf5(weight_windows_group, name); + WeightWindows::from_hdf5(weight_windows_group, name, mesh_map); } close_group(weight_windows_group); From 1074e77e1850e9ca5cc74d5e4253660462a3f9d1 Mon Sep 17 00:00:00 2001 From: GuySten Date: Tue, 7 Oct 2025 16:27:13 +0300 Subject: [PATCH 2/9] remove id mapping feature and fix some things --- include/openmc/mesh.h | 4 +--- include/openmc/weight_windows.h | 5 ++--- src/mesh.cpp | 23 ++++++++++------------- src/weight_windows.cpp | 12 ++++-------- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index a327f8bb335..035b438ac19 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -1082,9 +1082,7 @@ void read_meshes(pugi::xml_node root); //! Read meshes from a HDF5 file // //! \param[in] group HDF5 group -//! \param[out] mesh_map mapping from mesh ids inside HDF5 group to global mesh -//! ids -void read_meshes(hid_t group, std::unordered_map& mesh_map); +void read_meshes(hid_t group); //! Write mesh data to an HDF5 group // diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 25421ef4957..7638155228e 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -87,9 +87,8 @@ class WeightWindows { WeightWindows(pugi::xml_node node); ~WeightWindows(); static WeightWindows* create(int32_t id = -1); - static WeightWindows* from_hdf5(hid_t wws_group, - const std::string& group_name, - std::unordered_map& id_map); + static WeightWindows* from_hdf5( + hid_t wws_group, const std::string& group_name); //---------------------------------------------------------------------------- // Methods diff --git a/src/mesh.cpp b/src/mesh.cpp index 94b75f6eda8..a03ff56eb15 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3821,14 +3821,8 @@ void read_meshes(pugi::xml_node root) } } -void read_meshes(hid_t group, std::unordered_map& mesh_map) +void read_meshes(hid_t group) { - // Find the iterator to the element with the maximum key - auto max_key_it = - std::max_element(model::mesh_map.begin(), model::mesh_map.end(), - [](const auto& p1, const auto& p2) { return p1.first < p2.first; }); - auto offset = max_key_it->first; - std::unordered_set mesh_ids; std::array ids; @@ -3836,8 +3830,6 @@ void read_meshes(hid_t group, std::unordered_map& mesh_map) for (auto id : ids) { - mesh_map[id] = id + offset; - // Check to make sure multiple meshes in the same file don't share IDs if (contains(mesh_ids, id)) { fatal_error(fmt::format("Two or more meshes use the same unique ID " @@ -3846,11 +3838,18 @@ void read_meshes(hid_t group, std::unordered_map& mesh_map) } mesh_ids.insert(id); + // If we've already read a mesh with the same ID in a *different* file, + // assume it is the same here + if (model::mesh_map.find(id) != model::mesh_map.end()) { + warning(fmt::format("Mesh with ID={} appears in multiple files.", id)); + continue; + } + std::string name = fmt::format("mesh {}", id); hid_t mesh_group = open_group(group, name.c_str()); std::string mesh_type; - if (attribute_exists(mesh_group, "type")) { + if (object_exists(mesh_group, "type")) { read_dataset(mesh_group, "type", mesh_type); } else { mesh_type = "regular"; @@ -3858,7 +3857,7 @@ void read_meshes(hid_t group, std::unordered_map& mesh_map) // determine the mesh library to use std::string mesh_lib; - if (attribute_exists(mesh_group, "library")) { + if (object_exists(mesh_group, "library")) { read_dataset(mesh_group, "library", mesh_lib); } @@ -3888,8 +3887,6 @@ void read_meshes(hid_t group, std::unordered_map& mesh_map) fatal_error("Invalid mesh type: " + mesh_type); } - model::meshes.back()->id_ = mesh_map[model::meshes.back()->id_]; - // Map ID to position in vector model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1; } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 6084a02753b..0d648d3335e 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -241,8 +241,8 @@ WeightWindows* WeightWindows::create(int32_t id) return wws; } -WeightWindows* WeightWindows::from_hdf5(hid_t wws_group, - const std::string& group_name, std::unordered_map& id_map) +WeightWindows* WeightWindows::from_hdf5( + hid_t wws_group, const std::string& group_name) { // collect ID from the name of this group hid_t ww_group = open_group(wws_group, group_name); @@ -258,8 +258,6 @@ WeightWindows* WeightWindows::from_hdf5(hid_t wws_group, int32_t mesh_id; read_dataset(ww_group, "mesh", mesh_id); - mesh_id = id_map[mesh_id]; - if (model::mesh_map.count(mesh_id) == 0) { fatal_error( fmt::format("Mesh {} used in weight windows does not exist.", mesh_id)); @@ -1330,16 +1328,14 @@ extern "C" int openmc_weight_windows_import(const char* filename) hid_t weight_windows_group = open_group(ww_file, "weight_windows"); - std::unordered_map mesh_map; - hid_t mesh_group = open_group(ww_file, "meshes"); - read_meshes(mesh_group, mesh_map); + read_meshes(mesh_group); std::vector names = group_names(weight_windows_group); for (const auto& name : names) { - WeightWindows::from_hdf5(weight_windows_group, name, mesh_map); + WeightWindows::from_hdf5(weight_windows_group, name); } close_group(weight_windows_group); From 869b10754824ae01d2eb04368972d3283a34f410 Mon Sep 17 00:00:00 2001 From: GuySten Date: Thu, 16 Oct 2025 17:06:25 +0300 Subject: [PATCH 3/9] added tests for writing and reading mesh objects from HDF5 --- tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_mesh.cpp | 183 ++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 tests/cpp_unit_tests/test_mesh.cpp diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 8fedc2daa57..5f87db9eac2 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -5,6 +5,7 @@ set(TEST_NAMES test_interpolate test_math test_mcpl_stat_sum + test_mesh # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_mesh.cpp b/tests/cpp_unit_tests/test_mesh.cpp new file mode 100644 index 00000000000..a20a1b45e2d --- /dev/null +++ b/tests/cpp_unit_tests/test_mesh.cpp @@ -0,0 +1,183 @@ +#include +#include +#include + +#include +#include + +#include "openmc/hdf5_interface.h" +#include "openmc/mesh.h" + +using namespace openmc; + +TEST_CASE("Test mesh hdf5 roundtrip - regular") +{ + // The XML data as a string + std::string xml_string = R"( + + 3 4 5 + -2 -3 -5 + 2 3 5 + + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("mesh"); + + auto mesh = RegularMesh(root); + + hid_t file_id = file_open("mesh.h5", 'w'); + + mesh.to_hdf5(file_id); + + file_close(file_id); + + hid_t file_id2 = file_open("mesh.h5", 'r'); + + hid_t group = open_group(file_id2, "mesh 1"); + + auto mesh2 = RegularMesh(group); + + file_close(file_id2); + + remove("mesh.h5"); + + REQUIRE(mesh2.shape_ == mesh.shape_); + + REQUIRE(mesh2.lower_left() == mesh.lower_left()); + + REQUIRE(mesh2.upper_right() == mesh.upper_right()); +} + +TEST_CASE("Test mesh hdf5 roundtrip - rectilinear") +{ + // The XML data as a string + std::string xml_string = R"( + + 0.0 1.0 5.0 10.0 + -10.0 -5.0 0.0 + -100.0 0.0 100.0 + + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("mesh"); + + auto mesh = RectilinearMesh(root); + + hid_t file_id = file_open("mesh.h5", 'w'); + + mesh.to_hdf5(file_id); + + file_close(file_id); + + hid_t file_id2 = file_open("mesh.h5", 'r'); + + hid_t group = open_group(file_id2, "mesh 1"); + + auto mesh2 = RectilinearMesh(group); + + file_close(file_id2); + + remove("mesh.h5"); + + REQUIRE(mesh2.shape_ == mesh.shape_); + + REQUIRE(mesh2.grid_ == mesh.grid_); +} + +TEST_CASE("Test mesh hdf5 roundtrip - cylindrical") +{ + // The XML data as a string + std::string xml_string = R"( + + 0.1 0.2 0.5 1.0 + 0.0 6.283185307179586 + 0.1 0.2 0.4 0.6 1.0 + 0 0 0 + + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("mesh"); + + auto mesh = CylindricalMesh(root); + + hid_t file_id = file_open("mesh.h5", 'w'); + + mesh.to_hdf5(file_id); + + file_close(file_id); + + hid_t file_id2 = file_open("mesh.h5", 'r'); + + hid_t group = open_group(file_id2, "mesh 1"); + + auto mesh2 = CylindricalMesh(group); + + file_close(file_id2); + + remove("mesh.h5"); + + REQUIRE(mesh2.shape_ == mesh.shape_); + + REQUIRE(mesh2.grid_ == mesh.grid_); +} + +TEST_CASE("Test mesh hdf5 roundtrip - spherical") +{ + // The XML data as a string + std::string xml_string = R"( + + 0.1 0.2 0.5 1.0 + 0.0 3.141592653589793 + 0.0 6.283185307179586 + 0.0 0.0 0.0 + ' + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("mesh"); + + auto mesh = SphericalMesh(root); + + hid_t file_id = file_open("mesh.h5", 'w'); + + mesh.to_hdf5(file_id); + + file_close(file_id); + + hid_t file_id2 = file_open("mesh.h5", 'r'); + + hid_t group = open_group(file_id2, "mesh 1"); + + auto mesh2 = SphericalMesh(group); + + file_close(file_id2); + + remove("mesh.h5"); + + REQUIRE(mesh2.shape_ == mesh.shape_); + + REQUIRE(mesh2.grid_ == mesh.grid_); +} From 1784abcac7a011db6c383fb42cf6cec7716d228d Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 20 Oct 2025 15:57:38 -0500 Subject: [PATCH 4/9] Factor mesh object generation into static method on base class --- include/openmc/mesh.h | 4 ++ src/mesh.cpp | 94 +++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 56 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 035b438ac19..3b2868eb09c 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -135,6 +135,10 @@ class Mesh { Mesh(hid_t group); virtual ~Mesh() = default; + template + static const std::unique_ptr& create( + T dataset, const std::string& mesh_type, const std::string& mesh_library); + // Methods //! Perform any preparation needed to support point location within the mesh virtual void prepare_for_point_location() {}; diff --git a/src/mesh.cpp b/src/mesh.cpp index a03ff56eb15..021b2d96e35 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -230,6 +230,42 @@ void MaterialVolumes::add_volume_unsafe( // Mesh implementation //============================================================================== +template +const std::unique_ptr& Mesh::create( + T dataset, const std::string& mesh_type, const std::string& mesh_library) +{ + // Determine mesh type. Add to model vector and map + if (mesh_type == RegularMesh::mesh_type) { + model::meshes.push_back(make_unique(dataset)); + } else if (mesh_type == RectilinearMesh::mesh_type) { + model::meshes.push_back(make_unique(dataset)); + } else if (mesh_type == CylindricalMesh::mesh_type) { + model::meshes.push_back(make_unique(dataset)); + } else if (mesh_type == SphericalMesh::mesh_type) { + model::meshes.push_back(make_unique(dataset)); +#ifdef OPENMC_DAGMC_ENABLED + } else if (mesh_type == UnstructuredMesh::mesh_type && + mesh_library == MOABMesh::mesh_lib_type) { + model::meshes.push_back(make_unique(dataset)); +#endif +#ifdef OPENMC_LIBMESH_ENABLED + } else if (mesh_type == UnstructuredMesh::mesh_type && + mesh_library == LibMesh::mesh_lib_type) { + model::meshes.push_back(make_unique(dataset)); +#endif + } else if (mesh_type == UnstructuredMesh::mesh_type) { + fatal_error("Unstructured mesh support is not enabled or the mesh " + "library is invalid."); + } else { + fatal_error(fmt::format("Invalid mesh type: {}", mesh_type)); + } + + // Map ID to position in vector + model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1; + + return model::meshes.back(); +} + Mesh::Mesh(pugi::xml_node node) { // Read mesh id @@ -3790,34 +3826,7 @@ void read_meshes(pugi::xml_node root) mesh_lib = get_node_value(node, "library", true, true); } - // Read mesh and add to vector - if (mesh_type == RegularMesh::mesh_type) { - model::meshes.push_back(make_unique(node)); - } else if (mesh_type == RectilinearMesh::mesh_type) { - model::meshes.push_back(make_unique(node)); - } else if (mesh_type == CylindricalMesh::mesh_type) { - model::meshes.push_back(make_unique(node)); - } else if (mesh_type == SphericalMesh::mesh_type) { - model::meshes.push_back(make_unique(node)); -#ifdef OPENMC_DAGMC_ENABLED - } else if (mesh_type == UnstructuredMesh::mesh_type && - mesh_lib == MOABMesh::mesh_lib_type) { - model::meshes.push_back(make_unique(node)); -#endif -#ifdef OPENMC_LIBMESH_ENABLED - } else if (mesh_type == UnstructuredMesh::mesh_type && - mesh_lib == LibMesh::mesh_lib_type) { - model::meshes.push_back(make_unique(node)); -#endif - } else if (mesh_type == UnstructuredMesh::mesh_type) { - fatal_error("Unstructured mesh support is not enabled or the mesh " - "library is invalid."); - } else { - fatal_error("Invalid mesh type: " + mesh_type); - } - - // Map ID to position in vector - model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1; + Mesh::create(node, mesh_type, mesh_lib); } } @@ -3861,34 +3870,7 @@ void read_meshes(hid_t group) read_dataset(mesh_group, "library", mesh_lib); } - // Read mesh and add to vector - if (mesh_type == RegularMesh::mesh_type) { - model::meshes.push_back(make_unique(mesh_group)); - } else if (mesh_type == RectilinearMesh::mesh_type) { - model::meshes.push_back(make_unique(mesh_group)); - } else if (mesh_type == CylindricalMesh::mesh_type) { - model::meshes.push_back(make_unique(mesh_group)); - } else if (mesh_type == SphericalMesh::mesh_type) { - model::meshes.push_back(make_unique(mesh_group)); -#ifdef OPENMC_DAGMC_ENABLED - } else if (mesh_type == UnstructuredMesh::mesh_type && - mesh_lib == MOABMesh::mesh_lib_type) { - model::meshes.push_back(make_unique(mesh_group)); -#endif -#ifdef OPENMC_LIBMESH_ENABLED - } else if (mesh_type == UnstructuredMesh::mesh_type && - mesh_lib == LibMesh::mesh_lib_type) { - model::meshes.push_back(make_unique(mesh_group)); -#endif - } else if (mesh_type == UnstructuredMesh::mesh_type) { - fatal_error("Unstructured mesh support is not enabled or the mesh " - "library is invalid."); - } else { - fatal_error("Invalid mesh type: " + mesh_type); - } - - // Map ID to position in vector - model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1; + Mesh::create(mesh_group, mesh_type, mesh_lib); } } From 38c90d98e065ccf756c6776c3affe5706bbde093 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 20 Oct 2025 16:21:06 -0500 Subject: [PATCH 5/9] Adding a test for multiple mesh roundtrip --- tests/cpp_unit_tests/test_mesh.cpp | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/cpp_unit_tests/test_mesh.cpp b/tests/cpp_unit_tests/test_mesh.cpp index a20a1b45e2d..650a613681a 100644 --- a/tests/cpp_unit_tests/test_mesh.cpp +++ b/tests/cpp_unit_tests/test_mesh.cpp @@ -181,3 +181,72 @@ TEST_CASE("Test mesh hdf5 roundtrip - spherical") REQUIRE(mesh2.grid_ == mesh.grid_); } + +TEST_CASE("Test multiple meshes HDF5 roundtrip - spherical") +{ + // The XML data as a string + std::string xml_string = R"( + + + 0.1 0.2 0.5 1.0 + 0.0 3.141592653589793 + 0.0 6.283185307179586 + 0.0 0.0 0.0 + + + 3 4 5 + -2 -3 -5 + 2 3 5 + + + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("meshes"); + + read_meshes(root); + + const auto spherical_mesh_xml = + dynamic_cast(model::meshes[0].get()); + const auto regular_mesh_xml = + dynamic_cast(model::meshes[1].get()); + + hid_t file_id = file_open("meshes.h5", 'w'); + + hid_t root_group = create_group(file_id, "root"); + + open_group(file_id, "root"); + + meshes_to_hdf5(root_group); + + close_group(root_group); + + file_close(file_id); + + hid_t file_id2 = file_open("meshes.h5", 'r'); + + hid_t root_group_read = open_group(file_id2, "root"); + + hid_t mesh_group_read = open_group(root_group_read, "meshes"); + + read_meshes(mesh_group_read); + + const auto spherical_mesh_hdf5 = dynamic_cast( + model::meshes[model::mesh_map[spherical_mesh_xml->id_ + 10]].get()); + const auto regular_mesh_hdf5 = dynamic_cast( + model::meshes[model::mesh_map[regular_mesh_xml->id_ + 10]].get()); + + remove("meshes.h5"); + + REQUIRE(spherical_mesh_hdf5->shape_ == spherical_mesh_xml->shape_); + REQUIRE(spherical_mesh_hdf5->grid_ == spherical_mesh_xml->grid_); + + REQUIRE(regular_mesh_hdf5->shape_ == regular_mesh_xml->shape_); + REQUIRE(regular_mesh_hdf5->lower_left() == regular_mesh_xml->lower_left()); + REQUIRE(regular_mesh_hdf5->upper_right() == regular_mesh_xml->upper_right()); +} From 795b6d5f18a92bb24a5dd660f3efbc0d030c69c6 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 20 Oct 2025 21:25:36 -0500 Subject: [PATCH 6/9] Ensure more than one mesh is read from an HDF5 file. Remove assumption that mesh is at the back of the meshes vector in . --- src/mesh.cpp | 10 ++++++++-- tests/cpp_unit_tests/test_mesh.cpp | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index 021b2d96e35..11c5684abdd 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -312,7 +312,13 @@ void Mesh::set_id(int32_t id) // Update ID and entry in the mesh map id_ = id; - model::mesh_map[id] = model::meshes.size() - 1; + + // find the index of this mesh in the model::meshes vector + // (search in reverse because this mesh was likely just added to the vector) + auto it = std::find_if(model::meshes.rbegin(), model::meshes.rend(), + [this](const std::unique_ptr& mesh) { return mesh.get() == this; }); + + model::mesh_map[id] = std::distance(model::meshes.begin(), it.base()) - 1; } vector Mesh::volumes() const @@ -3834,7 +3840,7 @@ void read_meshes(hid_t group) { std::unordered_set mesh_ids; - std::array ids; + std::vector ids; read_attribute(group, "ids", ids); for (auto id : ids) { diff --git a/tests/cpp_unit_tests/test_mesh.cpp b/tests/cpp_unit_tests/test_mesh.cpp index 650a613681a..24c4f77373f 100644 --- a/tests/cpp_unit_tests/test_mesh.cpp +++ b/tests/cpp_unit_tests/test_mesh.cpp @@ -236,10 +236,15 @@ TEST_CASE("Test multiple meshes HDF5 roundtrip - spherical") read_meshes(mesh_group_read); + // increment mesh IDs to avoid collision during read + for (auto& mesh : model::meshes) { + mesh->set_id(mesh->id() + 10); + } + const auto spherical_mesh_hdf5 = dynamic_cast( - model::meshes[model::mesh_map[spherical_mesh_xml->id_ + 10]].get()); + model::meshes[model::mesh_map[spherical_mesh_xml->id_]].get()); const auto regular_mesh_hdf5 = dynamic_cast( - model::meshes[model::mesh_map[regular_mesh_xml->id_ + 10]].get()); + model::meshes[model::mesh_map[regular_mesh_xml->id_]].get()); remove("meshes.h5"); From 8286f4a400fcbd7656bffde4f7b2b0e4db5a8297 Mon Sep 17 00:00:00 2001 From: GuySten Date: Tue, 21 Oct 2025 10:56:37 +0300 Subject: [PATCH 7/9] refactored repeated RegularMesh checks in set_grid method --- include/openmc/mesh.h | 2 + src/mesh.cpp | 156 ++++++++++++++++++++---------------------- 2 files changed, 76 insertions(+), 82 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 3b2868eb09c..aa892f85dc6 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -489,6 +489,8 @@ class RegularMesh : public StructuredMesh { //! Return the volume for a given mesh index double volume(const MeshIndex& ijk) const override; + int set_grid(); + // Data members double volume_frac_; //!< Volume fraction of each mesh element double element_volume_; //!< Volume of each mesh element diff --git a/src/mesh.cpp b/src/mesh.cpp index 11c5684abdd..1bdce2d9dfe 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1179,83 +1179,62 @@ void StructuredMesh::surface_bins_crossed( // RegularMesh implementation //============================================================================== -RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} +int RegularMesh::set_grid() { - // Determine number of dimensions for mesh - if (!check_for_node(node, "dimension")) { - fatal_error("Must specify on a regular mesh."); - } - - xt::xtensor shape = get_node_xarray(node, "dimension"); - int n = n_dimension_ = shape.size(); - if (n != 1 && n != 2 && n != 3) { - fatal_error("Mesh must be one, two, or three dimensions."); - } - std::copy(shape.begin(), shape.end(), shape_.begin()); + auto shape = xt::adapt(shape_, {n_dimension_}); // Check that dimensions are all greater than zero if (xt::any(shape <= 0)) { - fatal_error("All entries on the element for a tally " - "mesh must be positive."); - } - - // Check for lower-left coordinates - if (check_for_node(node, "lower_left")) { - // Read mesh lower-left corner location - lower_left_ = get_node_xarray(node, "lower_left"); - } else { - fatal_error("Must specify on a mesh."); + set_errmsg("All entries for a regular mesh dimensions " + "must be positive."); + return OPENMC_E_INVALID_ARGUMENT; } // Make sure lower_left and dimension match - if (shape.size() != lower_left_.size()) { - fatal_error("Number of entries on must be the same " - "as the number of entries on ."); + if (shape_.size() != lower_left_.size()) { + set_errmsg("Number of entries in lower_left must be the same " + "as the regular mesh dimensions."); + return OPENMC_E_INVALID_ARGUMENT; } - - if (check_for_node(node, "width")) { - // Make sure one of upper-right or width were specified - if (check_for_node(node, "upper_right")) { - fatal_error("Cannot specify both and on a mesh."); - } - - width_ = get_node_xarray(node, "width"); + if (width_.size() > 0) { // Check to ensure width has same dimensions auto n = width_.size(); - if (n != lower_left_.size()) { - fatal_error("Number of entries on must be the same as " - "the number of entries on ."); + if (n != shape_.size()) { + set_errmsg("Number of entries on width must be the same as " + "the regular mesh dimensions."); + return OPENMC_E_INVALID_ARGUMENT; } // Check for negative widths if (xt::any(width_ < 0.0)) { - fatal_error("Cannot have a negative on a tally mesh."); + set_errmsg("Cannot have a negative width on a regular mesh."); + return OPENMC_E_INVALID_ARGUMENT; } // Set width and upper right coordinate upper_right_ = xt::eval(lower_left_ + shape * width_); - } else if (check_for_node(node, "upper_right")) { - upper_right_ = get_node_xarray(node, "upper_right"); + } else if (upper_right_.size() > 0) { - // Check to ensure width has same dimensions + // Check to ensure upper_right_ has same dimensions auto n = upper_right_.size(); - if (n != lower_left_.size()) { - fatal_error("Number of entries on must be the " - "same as the number of entries on ."); + if (n != shape_.size()) { + set_errmsg("Number of entries on upper_right must be the " + "same as the regular mesh dimensions."); + return OPENMC_E_INVALID_ARGUMENT; } // Check that upper-right is above lower-left if (xt::any(upper_right_ < lower_left_)) { - fatal_error("The coordinates must be greater than " - "the coordinates on a tally mesh."); + set_errmsg( + "The upper_right coordinates of a regular mesh must be greater than " + "the lower_left coordinates."); + return OPENMC_E_INVALID_ARGUMENT; } // Set width width_ = xt::eval((upper_right_ - lower_left_) / shape); - } else { - fatal_error("Must specify either or on a mesh."); } // Set material volumes @@ -1265,6 +1244,50 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} for (int i = 0; i < n_dimension_; i++) { element_volume_ *= width_[i]; } + return 0; +} + +RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} +{ + // Determine number of dimensions for mesh + if (!check_for_node(node, "dimension")) { + fatal_error("Must specify on a regular mesh."); + } + + xt::xtensor shape = get_node_xarray(node, "dimension"); + int n = n_dimension_ = shape.size(); + if (n != 1 && n != 2 && n != 3) { + fatal_error("Mesh must be one, two, or three dimensions."); + } + std::copy(shape.begin(), shape.end(), shape_.begin()); + + // Check for lower-left coordinates + if (check_for_node(node, "lower_left")) { + // Read mesh lower-left corner location + lower_left_ = get_node_xarray(node, "lower_left"); + } else { + fatal_error("Must specify on a mesh."); + } + + if (check_for_node(node, "width")) { + // Make sure one of upper-right or width were specified + if (check_for_node(node, "upper_right")) { + fatal_error("Cannot specify both and on a mesh."); + } + + width_ = get_node_xarray(node, "width"); + + } else if (check_for_node(node, "upper_right")) { + + upper_right_ = get_node_xarray(node, "upper_right"); + + } else { + fatal_error("Must specify either or on a mesh."); + } + + if (int err = set_grid()) { + fatal_error(openmc_err_msg); + } } RegularMesh::RegularMesh(hid_t group) : StructuredMesh {group} @@ -1282,55 +1305,24 @@ RegularMesh::RegularMesh(hid_t group) : StructuredMesh {group} } std::copy(shape.begin(), shape.end(), shape_.begin()); - // Check that dimensions are all greater than zero - if (xt::any(shape <= 0)) { - fatal_error("All entries on the element for a tally " - "mesh must be positive."); - } - // Check for lower-left coordinates if (object_exists(group, "lower_left")) { // Read mesh lower-left corner location read_dataset(group, "lower_left", lower_left_); } else { - fatal_error("Must specify on a mesh."); - } - - // Make sure lower_left and dimension match - if (shape.size() != lower_left_.size()) { - fatal_error("Number of entries on must be the same " - "as the number of entries on ."); + fatal_error("Must specify lower_left dataset on a mesh."); } if (object_exists(group, "upper_right")) { read_dataset(group, "upper_right", upper_right_); - // Check to ensure width has same dimensions - auto n = upper_right_.size(); - if (n != lower_left_.size()) { - fatal_error("Number of entries on must be the " - "same as the number of entries on ."); - } - - // Check that upper-right is above lower-left - if (xt::any(upper_right_ < lower_left_)) { - fatal_error("The coordinates must be greater than " - "the coordinates on a tally mesh."); - } - - // Set width - width_ = xt::eval((upper_right_ - lower_left_) / shape); } else { - fatal_error("Must specify either or on a mesh."); + fatal_error("Must specify either upper_right dataset on a mesh."); } - // Set material volumes - volume_frac_ = 1.0 / xt::prod(shape)(); - - element_volume_ = 1.0; - for (int i = 0; i < n_dimension_; i++) { - element_volume_ *= width_[i]; + if (int err = set_grid()) { + fatal_error(openmc_err_msg); } } From 3966ccbfe31d938a4ad6eef1367763e519a2c543 Mon Sep 17 00:00:00 2001 From: GuySten Date: Tue, 21 Oct 2025 12:00:48 +0300 Subject: [PATCH 8/9] bugfix --- src/mesh.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index 1bdce2d9dfe..610d057cf08 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1191,7 +1191,7 @@ int RegularMesh::set_grid() } // Make sure lower_left and dimension match - if (shape_.size() != lower_left_.size()) { + if (lower_left_.size() != n_dimension_) { set_errmsg("Number of entries in lower_left must be the same " "as the regular mesh dimensions."); return OPENMC_E_INVALID_ARGUMENT; @@ -1199,8 +1199,7 @@ int RegularMesh::set_grid() if (width_.size() > 0) { // Check to ensure width has same dimensions - auto n = width_.size(); - if (n != shape_.size()) { + if (width_.size() != n_dimension_) { set_errmsg("Number of entries on width must be the same as " "the regular mesh dimensions."); return OPENMC_E_INVALID_ARGUMENT; @@ -1218,8 +1217,7 @@ int RegularMesh::set_grid() } else if (upper_right_.size() > 0) { // Check to ensure upper_right_ has same dimensions - auto n = upper_right_.size(); - if (n != shape_.size()) { + if (upper_right_.size() != n_dimension_) { set_errmsg("Number of entries on upper_right must be the " "same as the regular mesh dimensions."); return OPENMC_E_INVALID_ARGUMENT; From 5afbae13bc01819f96f4e1325ad0e9cf41e981d1 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Tue, 21 Oct 2025 11:36:22 -0500 Subject: [PATCH 9/9] Adding some clarifying doc strings --- include/openmc/mesh.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index aa892f85dc6..5c9272e93b9 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -135,6 +135,7 @@ class Mesh { Mesh(hid_t group); virtual ~Mesh() = default; + // Factory method for creating meshes from either an XML node or HDF5 group template static const std::unique_ptr& create( T dataset, const std::string& mesh_type, const std::string& mesh_library); @@ -1085,9 +1086,9 @@ class AdaptiveLibMesh : public LibMesh { //! \param[in] root XML node void read_meshes(pugi::xml_node root); -//! Read meshes from a HDF5 file +//! Read meshes from an HDF5 file // -//! \param[in] group HDF5 group +//! \param[in] group HDF5 group ("meshes" group) void read_meshes(hid_t group); //! Write mesh data to an HDF5 group