diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 45922f3a7f..57106ce349 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,6 +35,9 @@ to use Open Cascade's file I/O capabilities in support of Quest applications. - ItemCollection and its child classes MapCollection, ListCollection, and IndexedCollection were moved from Sidre to core. The namespace prefix for these classes is now `axom::` instead of `axom::sidre`. The internal usage of these types within Sidre Datastore and Group is unchanged. +- `MFEMSidreDataCollection::LoadExternalData` now takes two optional string parameters, one that is a + filename (defaults to the `name` member variable) and the other is a `Group` path relative to the base of + the Data Collection itself (defaults to the root of the `DataStore`). ### Deprecated diff --git a/src/axom/sidre/core/MFEMSidreDataCollection.cpp b/src/axom/sidre/core/MFEMSidreDataCollection.cpp index 3b5806bebe..23da04568c 100644 --- a/src/axom/sidre/core/MFEMSidreDataCollection.cpp +++ b/src/axom/sidre/core/MFEMSidreDataCollection.cpp @@ -953,18 +953,51 @@ void MFEMSidreDataCollection::Load(const std::string& path, } } -void MFEMSidreDataCollection::LoadExternalData(const std::string& path) +void MFEMSidreDataCollection::LoadExternalData(const std::string& filename, + const std::string& group_name) { + // Use the user-provided group name or the DataCollection's base group + Group* grp = m_bp_grp->getDataStore()->getRoot(); + if(!group_name.empty()) + { + #if defined(AXOM_USE_MPI) && defined(MFEM_USE_MPI) + if(m_comm != MPI_COMM_NULL) + { + SLIC_ERROR( + "Loading external data with a group name is not supported in " + "parallel."); + } + #endif + + SLIC_ERROR_IF(!m_bp_grp->hasGroup(group_name), + axom::fmt::format( + "MFEMSidreDataCollection does not have a Sidre Group '{}'", + group_name)); + grp = m_bp_grp->getGroup(group_name); + } + + // Use the user-provided file name or the DataCollection's file name + std::string path = name; + if(!filename.empty()) + { + path = filename; + } + path = get_file_path(path); + #if defined(AXOM_USE_MPI) && defined(MFEM_USE_MPI) if(m_comm != MPI_COMM_NULL) { + // The conduit abstraction appears to automatically handle the ".root" + // suffix, but the IOManager does not, so it gets added here + using axom::utilities::string::endsWith; + std::string suffixedPath = endsWith(path, ".root") ? path : path + ".root"; IOManager reader(m_comm); - reader.loadExternalData(m_bp_grp->getDataStore()->getRoot(), path); + reader.loadExternalData(grp, suffixedPath); } else #endif { - m_bp_grp->loadExternalData(path); + grp->loadExternalData(path); } } diff --git a/src/axom/sidre/core/MFEMSidreDataCollection.hpp b/src/axom/sidre/core/MFEMSidreDataCollection.hpp index d90d8ca0e4..2d8b8bea8c 100644 --- a/src/axom/sidre/core/MFEMSidreDataCollection.hpp +++ b/src/axom/sidre/core/MFEMSidreDataCollection.hpp @@ -476,8 +476,14 @@ class MFEMSidreDataCollection : public mfem::DataCollection Load(get_file_path(name), "sidre_hdf5"); } - /// Load external data after registering externally owned fields. - void LoadExternalData(const std::string& path); + /** @brief Load external data for the whole MFEMSidreDataCollection unless a specific group name is given. + * @note This must happen after registering externally owned fields. + * + * @param filename Optional base filename to be loaded, function will add prefix path and cycle + * @param group_name Optional group name to load external data, relative to base of MFEMSidreDataCollection + **/ + void LoadExternalData(const std::string& filename = "", + const std::string& group_name = ""); /** @brief Updates the DataCollection's cycle, time, and time-step variables with the values from the data store. */ diff --git a/src/axom/sidre/tests/CMakeLists.txt b/src/axom/sidre/tests/CMakeLists.txt index f264f36589..ad0f9a1671 100644 --- a/src/axom/sidre/tests/CMakeLists.txt +++ b/src/axom/sidre/tests/CMakeLists.txt @@ -24,6 +24,7 @@ set(gtest_sidre_tests sidre_native_layout.cpp sidre_attribute.cpp sidre_mcarray.cpp + sidre_read_write_userdefined_data.cpp ) set(gtest_sidre_C_tests diff --git a/src/axom/sidre/tests/sidre_mfem_datacollection.cpp b/src/axom/sidre/tests/sidre_mfem_datacollection.cpp index d3bb3601f2..f20e683823 100644 --- a/src/axom/sidre/tests/sidre_mfem_datacollection.cpp +++ b/src/axom/sidre/tests/sidre_mfem_datacollection.cpp @@ -256,6 +256,59 @@ TEST(sidre_datacollection, dc_reload_gf_vdim) EXPECT_TRUE(sdc_reader.verifyMeshBlueprint()); } +TEST(sidre_datacollection, dc_reload_externaldata) +{ + const std::string view_name = "external_data"; + + // Create DC + auto mesh = mfem::Mesh::MakeCartesian1D(10); + const bool owns_mesh_data = true; + MFEMSidreDataCollection sdc_writer(testName(), &mesh, owns_mesh_data); + // After creation set owning to false so data doesn't get double free'd by reader and writer + sdc_writer.SetOwnData(false); +#if defined(AXOM_USE_MPI) && defined(MFEM_USE_MPI) + sdc_writer.SetComm(MPI_COMM_WORLD); +#endif + sdc_writer.SetCycle(0); + + // Create external buffer and add it to DC + axom::Array writer_data {1, 2, 3, 4}; + axom::sidre::Group* writer_bp_group = sdc_writer.GetBPGroup(); + axom::sidre::View* writer_external_view = + writer_bp_group->createView(view_name); + writer_external_view->setExternalDataPtr(axom::sidre::INT64_ID, + writer_data.size(), + writer_data.data()); + EXPECT_TRUE(writer_bp_group->hasView(view_name)); + + sdc_writer.Save(); + + // Load DC from file + MFEMSidreDataCollection sdc_reader(testName()); +#if defined(AXOM_USE_MPI) && defined(MFEM_USE_MPI) + sdc_reader.SetComm(MPI_COMM_WORLD); +#endif + // Note: this will recreate the external view but not load the external data yet + sdc_reader.Load(); + axom::sidre::Group* reader_bp_group = sdc_reader.GetBPGroup(); + EXPECT_TRUE(reader_bp_group->hasView(view_name)); + axom::sidre::View* reader_external_view = reader_bp_group->getView(view_name); + + // Create external buffer with wrong data and load previously saved data into it + axom::Array reader_data {5, 6, 7, 8}; + reader_external_view->setExternalDataPtr(reader_data.data()); + sdc_reader.LoadExternalData(); + + EXPECT_TRUE(writer_data.size() == reader_data.size()); + SLIC_INFO(axom::fmt::format("~~~~ {}", writer_data.size())); + for(int i = 0; i < reader_data.size(); ++i) + { + SLIC_INFO(axom::fmt::format("~~~~ {} == {}", reader_data[i], writer_data[i])); + EXPECT_TRUE(reader_data[i] == writer_data[i]); + } + SLIC_INFO("~~~ END"); +} + TEST(sidre_datacollection, dc_reload_mesh) { const std::string field_name = "test_field"; diff --git a/src/axom/sidre/tests/sidre_read_write_userdefined_data.cpp b/src/axom/sidre/tests/sidre_read_write_userdefined_data.cpp new file mode 100644 index 0000000000..ea706dcb96 --- /dev/null +++ b/src/axom/sidre/tests/sidre_read_write_userdefined_data.cpp @@ -0,0 +1,706 @@ +// Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "gtest/gtest.h" +#include "axom/sidre.hpp" + +using axom::sidre::DataStore; +using axom::sidre::Group; +using axom::sidre::IndexType; +#ifdef AXOM_USE_MFEM +using axom::sidre::MFEMSidreDataCollection; +#endif +using axom::sidre::View; + +// This test is meant to mock how Serac stores and loads Quadrature Data. +// The data is stored in Axom Array's of material states and are a user defined +// structure of POD types. +// +// There are two types of tests, one is where the data is managed by Sidre and +// the other is where the data is external to sidre and is saved and loaded to +// disk. The latter is what Serac is doing. + +//-----Mock Serac Structs------------------------------------------------------ + +/** + * @brief Arbitrary-rank tensor class + * @tparam T The type stored at each index + * @tparam n The dimensions of the tensor + */ +template +struct tensor; + +template +struct tensor +{ + template + constexpr auto& operator()(i_type i) + { + return data[i]; + } + template + constexpr auto& operator()(i_type i) const + { + return data[i]; + } + template + constexpr auto& operator()(i_type i, jklm_type... jklm) + { + return data[i](jklm...); + } + template + constexpr auto& operator()(i_type i, jklm_type... jklm) const + { + return data[i](jklm...); + } + + constexpr auto& operator[](int i) { return data[i]; } + constexpr const auto& operator[](int i) const { return data[i]; } + + tensor data[m]; +}; + +template +struct tensor +{ + template + constexpr auto& operator()(i_type i) + { + return data[i]; + } + template + constexpr auto& operator()(i_type i) const + { + return data[i]; + } + constexpr auto& operator[](int i) { return data[i]; } + constexpr const auto& operator[](int i) const { return data[i]; } + + template ::type> + constexpr operator T() + { + return data[0]; + } + + template ::type> + constexpr operator T() const + { + return data[0]; + } + + T data[m]; +}; + +//--------------------- + +template +bool check(const T&, double) +{ + SLIC_ERROR("You didn't implement a specialization for check"); + return false; +} + +template +void fill(T&, double) +{ + SLIC_ERROR("You didn't implement a specialization for fill"); +} + +//--------------------- + +template <> +bool check(const double& state, double value) +{ + SLIC_INFO(axom::fmt::format("{} == {}", state, value)); + return state == value; +} + +template <> +void fill(double& state, double value) +{ + state = value; +} + +//--------------------- + +struct StateOne +{ + double x; +}; + +template <> +bool check(const StateOne& state, double value) +{ + return state.x == value; +} + +template <> +void fill(StateOne& state, double value) +{ + state.x = value; +} + +//--------------------- + +struct StateTwo +{ + double x; + double y; +}; + +template <> +bool check(const StateTwo& state, double value) +{ + return (state.x == value) && (state.y == (value + 1)); +} + +template <> +void fill(StateTwo& state, double value) +{ + state.x = value; + state.y = value + 1; +} + +//--------------------- + +struct StateThree +{ + double x; + double y; + double z; +}; + +template <> +bool check(const StateThree& state, double value) +{ + return (state.x == value) && (state.y == (value + 1)) && + (state.z == (value + 2)); +} + +template <> +void fill(StateThree& state, double value) +{ + state.x = value; + state.y = value + 1; + state.z = value + 2; +} + +//--------------------- + +struct StateTensorSmall +{ + tensor t; + double x; +}; + +template <> +bool check(const StateTensorSmall& state, double value) +{ + return (state.t(0) == value) && (state.t(1) == (value + 1)) && + (state.x == (value + 4)); +} + +template <> +void fill(StateTensorSmall& state, double value) +{ + state.t(0) = value; + state.t(1) = value + 1; + state.x = value + 4; +} + +//--------------------- + +struct StateTensorLarge +{ + tensor t; + double x; +}; + +template <> +bool check(const StateTensorLarge& state, double value) +{ + return (state.t(0, 0) == value) && (state.t(0, 1) == (value + 1)) && + (state.t(1, 0) == (value + 2)) && (state.t(1, 1) == (value + 3)) && + (state.x == (value + 4)); +} + +template <> +void fill(StateTensorLarge& state, double value) +{ + state.t(0, 0) = value; + state.t(0, 1) = value + 1; + state.t(1, 0) = value + 2; + state.t(1, 1) = value + 3; + state.x = value + 4; +} + +//--------------------- + +template +void fill_array(T, int, bool) +{ + SLIC_ERROR("You didn't implement the fill array of this type."); +} + +template +void check_array(T) +{ + SLIC_ERROR("You didn't implement the check array of this type."); +} + +template +void fill_array(axom::Array& states, double fill_value, bool increment) +{ + int count = 0; + for(auto& state : states) + { + fill(state, fill_value); + if(increment) + { + fill_value++; + } + count++; + } + SLIC_INFO( + axom::fmt::format("filled {} states with end value {}", count, fill_value)); +} + +template +void check_array(axom::Array states) +{ + int count = 0; + double check_value = 0; + for(auto& state : states) + { + EXPECT_TRUE(check(state, check_value++)); + count++; + } + SLIC_INFO( + axom::fmt::format("checked {} states with end value {}", count, check_value)); +} + +//------------------------------------------------------------------------------ + +template +void test_user_defined_data() +{ + // populate data + constexpr IndexType size = 10; + axom::Array states(size, size); + fill_array(states, 0, true); + + // Create datastore + DataStore ds; + Group* root = ds.getRoot(); + + // get size + auto num_states = static_cast(states.size()); + auto state_size = static_cast(sizeof(*(states.begin()))); + auto total_size = num_states * state_size; + + SLIC_INFO(axom::fmt::format("Num of States={}", num_states)); + SLIC_INFO(axom::fmt::format("State Size={}", state_size)); + SLIC_INFO(axom::fmt::format("Total Size={}", total_size)); + SLIC_INFO( + axom::fmt::format("Total Size/State Size={}", total_size / state_size)); + + // write shape + root->createViewScalar("num_states", num_states); + root->createViewScalar("state_size", state_size); + root->createViewScalar("total_size", total_size); + + // write data to datastore as bytes + View* states_view = + root->createViewAndAllocate("states", axom::sidre::UINT8_ID, total_size); + std::uint8_t* sidre_state_data = states_view->getData(); + memcpy(sidre_state_data, states.data(), static_cast(total_size)); + + // mess with data before overriding it back to original in sidre + fill_array(states, -1, false); + + // Copy original data back over local data + memcpy(states.data(), sidre_state_data, static_cast(total_size)); + + // Test data is back to original + check_array(states); +} + +template +void test_external_user_defined_data() +{ + // populate data + constexpr IndexType size = 10; + axom::Array states(size, size); + fill_array(states, 0, true); + + // Create datastore + DataStore ds; + Group* root = ds.getRoot(); + + // get size + auto num_states = static_cast(states.size()); + auto state_size = static_cast(sizeof(*(states.begin()))); + auto total_size = num_states * state_size; + auto num_uint8s = total_size / sizeof(std::uint8_t); + + SLIC_INFO(axom::fmt::format("Num of States={}", num_states)); + SLIC_INFO(axom::fmt::format("State Size={}", state_size)); + SLIC_INFO(axom::fmt::format("Total Size={}", total_size)); + SLIC_INFO( + axom::fmt::format("Total Size/State Size={}", total_size / state_size)); + SLIC_INFO(axom::fmt::format("Num of uint8={}", num_uint8s)); + + // write shape + root->createViewScalar("num_states", num_states); + root->createViewScalar("state_size", state_size); + root->createViewScalar("total_size", total_size); + + // Add states as an external buffer + View* states_view = root->createView("states"); + states_view->setExternalDataPtr(axom::sidre::UINT8_ID, + num_uint8s, + states.data()); + + // Save the array data into a file + std::string filename = "sidre_external_states"; + root->save(filename); + + // Load data into new array + Group* loaded_group = root->createGroup("loaded_data"); + loaded_group->load(filename); + + // Verify size correctness + EXPECT_TRUE(loaded_group->hasView("num_states")); + auto loaded_num_states = + loaded_group->getView("num_states")->getData(); + EXPECT_TRUE(num_states == loaded_num_states); + + EXPECT_TRUE(loaded_group->hasView("state_size")); + auto loaded_state_size = + loaded_group->getView("state_size")->getData(); + EXPECT_TRUE(state_size == loaded_state_size); + + EXPECT_TRUE(loaded_group->hasView("total_size")); + auto loaded_total_size = + loaded_group->getView("total_size")->getData(); + EXPECT_TRUE(total_size == loaded_total_size); + + // Create new array to fill with loaded data + axom::Array loaded_states(size, size); + fill_array(loaded_states, -1, false); + + // Load external data + EXPECT_TRUE(loaded_group->hasView("states")); + View* loaded_states_view = loaded_group->getView("states"); + loaded_states_view->setExternalDataPtr(loaded_states.data()); + loaded_group->loadExternalData(filename); + + // Test data was read into the new location and overwrote the bad data + check_array(loaded_states); +} + +#ifdef AXOM_USE_MFEM + +template +void test_MFEMSidreDataCollection_user_defined_data() +{ + std::string test_name = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + + // populate data + constexpr IndexType size = 10; + axom::Array states(size, size); + fill_array(states, 0, true); + + // Create MFEMSidreDataCollection + auto* mesh = new mfem::Mesh(mfem::Mesh::MakeCartesian1D(10)); + const bool owns_mesh_data = true; + MFEMSidreDataCollection sdc_writer(test_name, mesh, owns_mesh_data); + Group* bp_group = sdc_writer.GetBPGroup(); + + // get size + auto num_states = static_cast(states.size()); + auto state_size = static_cast(sizeof(*(states.begin()))); + auto total_size = num_states * state_size; + auto num_uint8s = total_size / sizeof(std::uint8_t); + + SLIC_INFO(axom::fmt::format("Num of States={}", num_states)); + SLIC_INFO(axom::fmt::format("State Size={}", state_size)); + SLIC_INFO(axom::fmt::format("Total Size={}", total_size)); + SLIC_INFO( + axom::fmt::format("Total Size/State Size={}", total_size / state_size)); + SLIC_INFO(axom::fmt::format("Num of uint8={}", num_uint8s)); + + // write shape + bp_group->createViewScalar("num_states", num_states); + bp_group->createViewScalar("state_size", state_size); + bp_group->createViewScalar("total_size", total_size); + + // Add states as an external buffer + View* states_view = bp_group->createView("states"); + states_view->setExternalDataPtr(axom::sidre::UINT8_ID, + num_uint8s, + states.data()); + + // Save the array data into a file + #if defined(AXOM_USE_MPI) && defined(MFEM_USE_MPI) + sdc_writer.SetComm(MPI_COMM_WORLD); + #endif + sdc_writer.SetCycle(0); + sdc_writer.Save(); + + // Load data into new Data Collection + MFEMSidreDataCollection sdc_reader(test_name); + #if defined(AXOM_USE_MPI) && defined(MFEM_USE_MPI) + sdc_reader.SetComm(MPI_COMM_WORLD); + #endif + sdc_reader.Load(); + + Group* loaded_bp_group = sdc_reader.GetBPGroup(); + + // Verify size correctness + EXPECT_TRUE(loaded_bp_group->hasView("num_states")); + auto loaded_num_states = + loaded_bp_group->getView("num_states")->getData(); + EXPECT_TRUE(num_states == loaded_num_states); + + EXPECT_TRUE(loaded_bp_group->hasView("state_size")); + auto loaded_state_size = + loaded_bp_group->getView("state_size")->getData(); + EXPECT_TRUE(state_size == loaded_state_size); + + EXPECT_TRUE(loaded_bp_group->hasView("total_size")); + auto loaded_total_size = + loaded_bp_group->getView("total_size")->getData(); + EXPECT_TRUE(total_size == loaded_total_size); + + // Create new array to fill with loaded data + axom::Array loaded_states(size, size); + fill_array(loaded_states, -1, false); + + // Load external data + EXPECT_TRUE(loaded_bp_group->hasView("states")); + View* loaded_states_view = loaded_bp_group->getView("states"); + loaded_states_view->setExternalDataPtr(loaded_states.data()); + sdc_reader.LoadExternalData(); + + // Test data was read into the new location and overwrote the bad data + check_array(loaded_states); +} + +#endif // AXOM_USE_MFEM + +//------------------------------------------------------------------------------ + +TEST(sidre, OneD_double_readandwrite) { test_user_defined_data(); } + +TEST(sidre, OneD_StateOne_readandwrite) +{ + test_user_defined_data(); +} + +TEST(sidre, OneD_StateTwo_readandwrite) +{ + test_user_defined_data(); +} + +TEST(sidre, OneD_StateThree_readandwrite) +{ + test_user_defined_data(); +} + +TEST(sidre, OneD_StateTensorSmall_readandwrite) +{ + test_user_defined_data(); +} + +TEST(sidre, OneD_StateTensorLarge_readandwrite) +{ + test_user_defined_data(); +} + +//------------------------- + +TEST(sidre, TwoD_double_readandwrite) { test_user_defined_data(); } + +TEST(sidre, TwoD_StateOne_readandwrite) +{ + test_user_defined_data(); +} + +TEST(sidre, TwoD_StateTwo_readandwrite) +{ + test_user_defined_data(); +} + +TEST(sidre, TwoD_StateThree_readandwrite) +{ + test_user_defined_data(); +} + +TEST(sidre, TwoD_StateTensorSmall_readandwrite) +{ + test_user_defined_data(); +} + +TEST(sidre, TwoD_StateTensorLarge_readandwrite) +{ + test_user_defined_data(); +} + +//------------------------------------------------------------------------------ + +TEST(sidre, OneD_double_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, OneD_StateOne_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, OneD_StateTwo_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, OneD_StateThree_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, OneD_StateTensorSmall_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, OneD_StateTensorLarge_external_readandwrite) +{ + test_external_user_defined_data(); +} + +//------------------------- + +TEST(sidre, TwoD_double_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, TwoD_StateOne_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, TwoD_StateTwo_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, TwoD_StateThree_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, TwoD_StateTensorSmall_external_readandwrite) +{ + test_external_user_defined_data(); +} + +TEST(sidre, TwoD_StateTensorLarge_external_readandwrite) +{ + test_external_user_defined_data(); +} + +//------------------------- + +#ifdef AXOM_USE_MFEM + +TEST(sidre, OneD_double_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, OneD_StateOne_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, OneD_StateTwo_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, OneD_StateThree_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, OneD_StateTensorSmall_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, OneD_StateTensorLarge_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +//------------------------- + +TEST(sidre, TwoD_double_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, TwoD_StateOne_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, TwoD_StateTwo_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, TwoD_StateThree_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, TwoD_StateTensorSmall_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +TEST(sidre, TwoD_StateTensorLarge_MFEMSidreDataCollection_readandwrite) +{ + test_MFEMSidreDataCollection_user_defined_data(); +} + +#endif // AXOM_USE_MFEM + +//---------------------------------------------------------------------- +int main(int argc, char* argv[]) +{ + int result = 0; + + ::testing::InitGoogleTest(&argc, argv); + axom::slic::SimpleLogger logger; + +#ifdef AXOM_USE_MPI + MPI_Init(&argc, &argv); +#endif + + result = RUN_ALL_TESTS(); + +#ifdef AXOM_USE_MPI + MPI_Finalize(); +#endif + + return result; +} diff --git a/src/axom/sidre/tests/spio/spio_parallel.hpp b/src/axom/sidre/tests/spio/spio_parallel.hpp index 974e7d5241..6f4e735271 100644 --- a/src/axom/sidre/tests/spio/spio_parallel.hpp +++ b/src/axom/sidre/tests/spio/spio_parallel.hpp @@ -359,7 +359,7 @@ TEST(spio_parallel, external_writeread) View* view2 = root2->getView("fields2/b/external_undescribed"); view2->setExternalDataPtr(restored_vals2); - reader.loadExternalData(root2, "out_spio_external_write_read.root"); + reader.loadExternalData(root2, file_name + ROOT_EXT); enum SpioTestResult { @@ -420,6 +420,174 @@ TEST(spio_parallel, external_writeread) delete ds2; } +//------------------------------------------------------------------------------ +TEST(spio_parallel, external_piecemeal_writeread) +{ + if(PROTOCOL != "sidre_hdf5") + { + SUCCEED() << "Loading external data in spio only currently supported " + << " for 'sidre_hdf5' protocol"; + return; + } + + int my_rank; + MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); + + int num_ranks; + MPI_Comm_size(MPI_COMM_WORLD, &num_ranks); + + const int num_output = numOutputFiles(num_ranks); + + const int nvals = 10; + int orig_vals1[nvals], orig_vals2[nvals]; + for(int i = 0; i < 10; i++) + { + orig_vals1[i] = (i + 10) * (404 - my_rank - i); + orig_vals2[i] = (i + 10) * (404 - my_rank - i) + 20; + } + + /* + * Create a DataStore and give it a small hierarchy of groups and views. + * + * The views are filled with repeatable nonsense data that will vary based + * on rank. + */ + DataStore* ds1 = new DataStore(); + + Group* root1 = ds1->getRoot(); + + Group* flds1 = root1->createGroup("fields1"); + Group* flds2 = root1->createGroup("fields2"); + + flds1->createView("external_array", axom::sidre::INT_ID, nvals, orig_vals1); + flds2->createView("external_array", axom::sidre::INT_ID, nvals, orig_vals2); + + /* + * Contents of the DataStore written to files with IOManager. + */ + const int num_files = num_output; + IOManager writer(MPI_COMM_WORLD); + + const std::string file_name = "out_spio_external_piecemeal_write_read"; + + writer.write(root1, num_files, file_name, PROTOCOL); + + /* + * Create another DataStore than holds nothing but the root group. + */ + DataStore* ds2 = new DataStore(); + Group* root2 = ds2->getRoot(); + + // pollute values so we know they are changed + int restored_vals1[nvals], restored_vals2[nvals]; + for(int i = 0; i < nvals; ++i) + { + restored_vals1[i] = -1; + restored_vals2[i] = -1; + } + + /* + * Read from the files that were written above. + */ + IOManager reader(MPI_COMM_WORLD); + + reader.read(root2, file_name + ROOT_EXT); + + // Swap these two sections to show lack of piecemeal loading + + // Section 1: (Works) Load all external arrays at a single time + EXPECT_TRUE(root2->hasGroup("fields1")); + flds1 = root2->getGroup("fields1"); + EXPECT_TRUE(flds1->hasView("external_array")); + View* view1 = flds1->getView("external_array"); + view1->setExternalDataPtr(restored_vals1); + + EXPECT_TRUE(root2->hasGroup("fields2")); + flds2 = root2->getGroup("fields2"); + EXPECT_TRUE(flds2->hasView("external_array")); + View* view2 = flds2->getView("external_array"); + view2->setExternalDataPtr(restored_vals2); + + reader.loadExternalData(root2, file_name + ROOT_EXT); + + // TODO: convert test to this when functionality works + // Section 2: (Doesn't work) Load external arrays one at a time + // EXPECT_TRUE(root2->hasGroup("fields1")); + // flds1 = root2->getGroup("fields1"); + // EXPECT_TRUE(flds1->hasView("external_array")); + // View* view1 = flds1->getView("external_array"); + // view1->setExternalDataPtr(restored_vals1); + // reader.loadExternalData(flds1, file_name + ROOT_EXT); + + // EXPECT_TRUE(root2->hasGroup("fields2")); + // flds2 = root2->getGroup("fields2"); + // EXPECT_TRUE(flds2->hasView("external_array")); + // View* view2 = flds2->getView("external_array"); + // view2->setExternalDataPtr(restored_vals2); + // reader.loadExternalData(flds2, file_name + ROOT_EXT); + + enum SpioTestResult + { + SPIO_TEST_SUCCESS = 0, + HIERARCHY_ERROR = 1 << 0, + EXT_ARRAY_ERROR = 1 << 1, + }; + int result = SPIO_TEST_SUCCESS; + + /* + * Verify that the contents of ds2 match those written from ds. + */ + EXPECT_TRUE(ds2->getRoot()->isEquivalentTo(root1)); + if(!ds2->getRoot()->isEquivalentTo(root1)) + { + result |= HIERARCHY_ERROR; + } + SLIC_WARNING_IF(result & HIERARCHY_ERROR, "Tree layouts don't match"); + + EXPECT_EQ(view1->getNumElements(), nvals); + if(view1->getNumElements() != nvals) + { + result |= EXT_ARRAY_ERROR; + } + else + { + for(int i = 0; i < nvals; ++i) + { + EXPECT_EQ(orig_vals1[i], restored_vals1[i]); + if(orig_vals1[i] != restored_vals1[i]) + { + result |= EXT_ARRAY_ERROR; + break; + } + } + } + SLIC_WARNING_IF(result & EXT_ARRAY_ERROR, + "External_array1 was not correctly loaded"); + + EXPECT_EQ(view2->getNumElements(), nvals); + if(view2->getNumElements() != nvals) + { + result |= EXT_ARRAY_ERROR; + } + else + { + for(int i = 0; i < nvals; ++i) + { + EXPECT_EQ(orig_vals2[i], restored_vals2[i]); + if(orig_vals2[i] != restored_vals2[i]) + { + result |= EXT_ARRAY_ERROR; + break; + } + } + } + SLIC_WARNING_IF(result & EXT_ARRAY_ERROR, + "External_array2 was not correctly loaded"); + + delete ds1; + delete ds2; +} + //---------------------------------------------------------------------- TEST(spio_parallel, irregular_writeread) { @@ -726,6 +894,8 @@ TEST(spio_parallel, parallel_increase_procs) MPI_Comm_split(MPI_COMM_WORLD, my_rank, 0, &split_comm); } + const std::string file_name = "out_spio_parallel_increase_procs"; + DataStore* ds = new DataStore(); if(my_rank <= top_output_rank) { @@ -748,8 +918,6 @@ TEST(spio_parallel, parallel_increase_procs) int num_files = 1; axom::sidre::IOManager writer(split_comm); - const std::string file_name = "out_spio_parallel_increase_procs"; - writer.write(root, num_files, file_name, PROTOCOL); } @@ -763,8 +931,7 @@ TEST(spio_parallel, parallel_increase_procs) IOManager reader(MPI_COMM_WORLD); - const std::string root_name = "out_spio_parallel_increase_procs.root"; - reader.read(ds2->getRoot(), root_name); + reader.read(ds2->getRoot(), file_name + ROOT_EXT); /* * Verify that the contents of ds2 on rank 0 match those written from ds. @@ -890,8 +1057,7 @@ TEST(spio_parallel, parallel_decrease_procs) { IOManager reader(split_comm); - const std::string root_name = "out_spio_parallel_decrease_procs.root"; - reader.read(ds2->getRoot(), root_name); + reader.read(ds2->getRoot(), file_name + ROOT_EXT); Group* ds2_root = ds2->getRoot(); @@ -1021,7 +1187,7 @@ TEST(spio_parallel, sidre_simple_blueprint_example) // Add the bp index to the root file writer.writeBlueprintIndexToRootFile(&ds, "mesh", - "out_spio_blueprint_example.root", + file_name + ROOT_EXT, "mesh"); #endif // AXOM_USE_HDF5