diff --git a/include/openPMD/RecordComponent.hpp b/include/openPMD/RecordComponent.hpp index d7fad18751..7b298cc6b4 100644 --- a/include/openPMD/RecordComponent.hpp +++ b/include/openPMD/RecordComponent.hpp @@ -127,6 +127,16 @@ class RecordComponent : public BaseRecordComponent template< typename T > RecordComponent& makeEmpty( uint8_t dimensions ); + /** + * @brief Non-template overload of RecordComponent::makeEmpty(). + * Uses the passed openPMD datatype to determine the template parameter. + * + * @param dt The datatype of which to create an empty dataset. + * @param dimensions The dimensionality of the dataset. + * @return RecordComponent& + */ + RecordComponent& makeEmpty( Datatype dt, uint8_t dimensions ); + /** Returns true if this is an empty record component * * An empty record component has a defined dimensionality but zero extent diff --git a/src/RecordComponent.cpp b/src/RecordComponent.cpp index 8e1efba8df..657d906cf2 100644 --- a/src/RecordComponent.cpp +++ b/src/RecordComponent.cpp @@ -78,6 +78,32 @@ RecordComponent::getExtent() const return m_dataset->extent; } +namespace detail +{ +struct MakeEmpty +{ + template< typename T > + RecordComponent& operator()( RecordComponent & rc, uint8_t dimensions ) + { + return rc.makeEmpty< T >( dimensions ); + } + + template< unsigned int N > + RecordComponent& operator()( RecordComponent &, uint8_t ) + { + throw std::runtime_error( + "RecordComponent::makeEmpty: Unknown datatype." ); + } +}; +} + +RecordComponent& +RecordComponent::makeEmpty( Datatype dt, uint8_t dimensions ) +{ + static detail::MakeEmpty me; + return switchType< RecordComponent & >( dt, me, *this, dimensions ); +} + RecordComponent& RecordComponent::makeEmpty( Dataset d ) { diff --git a/src/binding/python/RecordComponent.cpp b/src/binding/python/RecordComponent.cpp index d474f4ea3a..2b235435eb 100644 --- a/src/binding/python/RecordComponent.cpp +++ b/src/binding/python/RecordComponent.cpp @@ -546,6 +546,20 @@ void init_RecordComponent(py::module &m) { py::arg("value")) .def("make_constant", &RecordComponent::makeConstant, py::arg("value")) + .def("make_empty", + []( RecordComponent & rc, Datatype dt, uint8_t dimensionality ) + { + return rc.makeEmpty( dt, dimensionality ); + }, + py::arg("datatype"), py::arg("dimensionality")) + .def("make_empty", + []( + RecordComponent & rc, + pybind11::dtype const dt, + uint8_t dimensionality ) + { + return rc.makeEmpty( dtype_from_numpy( dt ), dimensionality ); + }) // TODO if we also want to support scalar arrays, we have to switch // py::array for py::buffer as in Attributable diff --git a/test/SerialIOTest.cpp b/test/SerialIOTest.cpp index 86f8a98b52..4840101b59 100644 --- a/test/SerialIOTest.cpp +++ b/test/SerialIOTest.cpp @@ -286,6 +286,10 @@ empty_dataset_test( std::string file_ending ) series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_dim_7_int" ]; auto makeEmpty_dim_7_long = series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_dim_7_bool" ]; + auto makeEmpty_dim_7_int_alt = + series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_dim_7_int_alt" ]; + auto makeEmpty_dim_7_long_alt = + series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_dim_7_bool_alt" ]; auto makeEmpty_resetDataset_dim3 = series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_resetDataset_dim3" ]; auto makeEmpty_resetDataset_dim3_notallzero = @@ -293,6 +297,8 @@ empty_dataset_test( std::string file_ending ) .meshes[ "rho" ][ "makeEmpty_resetDataset_dim3_notallzero" ]; makeEmpty_dim_7_int.makeEmpty< int >( 7 ); makeEmpty_dim_7_long.makeEmpty< long >( 7 ); + makeEmpty_dim_7_int_alt.makeEmpty( Datatype::INT, 7 ); + makeEmpty_dim_7_long_alt.makeEmpty( Datatype::LONG, 7 ); makeEmpty_resetDataset_dim3.resetDataset( Dataset( Datatype::LONG, Extent( 3, 0 ) ) ); makeEmpty_resetDataset_dim3_notallzero.resetDataset( @@ -315,6 +321,10 @@ empty_dataset_test( std::string file_ending ) series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_dim_7_int" ]; auto makeEmpty_dim_7_long = series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_dim_7_bool" ]; + auto makeEmpty_dim_7_int_alt = + series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_dim_7_int_alt" ]; + auto makeEmpty_dim_7_long_alt = + series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_dim_7_bool_alt" ]; auto makeEmpty_resetDataset_dim3 = series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_resetDataset_dim3" ]; auto makeEmpty_resetDataset_dim3_notallzero = @@ -328,6 +338,14 @@ empty_dataset_test( std::string file_ending ) REQUIRE(makeEmpty_dim_7_long.getExtent() == Extent(7, 0)); REQUIRE(isSame(makeEmpty_dim_7_long.getDatatype(), determineDatatype< long >())); + REQUIRE(makeEmpty_dim_7_int_alt.getDimensionality() == 7); + REQUIRE(makeEmpty_dim_7_int_alt.getExtent() == Extent(7, 0)); + REQUIRE(isSame(makeEmpty_dim_7_int_alt.getDatatype(), determineDatatype< int >())); + + REQUIRE(makeEmpty_dim_7_long_alt.getDimensionality() == 7); + REQUIRE(makeEmpty_dim_7_long_alt.getExtent() == Extent(7, 0)); + REQUIRE(isSame(makeEmpty_dim_7_long_alt.getDatatype(), determineDatatype< long >())); + REQUIRE(makeEmpty_resetDataset_dim3.getDimensionality() == 3); REQUIRE(makeEmpty_resetDataset_dim3.getExtent() == Extent(3, 0)); REQUIRE(isSame(makeEmpty_resetDataset_dim3.getDatatype(), Datatype::LONG)); diff --git a/test/python/unittest/API/APITest.py b/test/python/unittest/API/APITest.py index 93e9b9ad4a..d24284367d 100644 --- a/test/python/unittest/API/APITest.py +++ b/test/python/unittest/API/APITest.py @@ -550,6 +550,162 @@ def testConstantRecords(self): for ext in io.file_extensions: self.makeConstantRoundTrip(ext) + def makeEmptyRoundTrip(self, file_ending): + # write + series = io.Series( + "unittest_py_empty_API." + file_ending, + io.Access_Type.create + ) + + ms = series.iterations[0].meshes + SCALAR = io.Mesh_Record_Component.SCALAR + DT = io.Datatype + + ms["CHAR"][SCALAR].make_empty(DT.CHAR, 1) + ms["UCHAR"][SCALAR].make_empty(DT.UCHAR, 2) + ms["SHORT"][SCALAR].make_empty(DT.SHORT, 3) + ms["INT"][SCALAR].make_empty(DT.INT, 4) + ms["LONG"][SCALAR].make_empty(DT.LONG, 5) + ms["LONGLONG"][SCALAR].make_empty(DT.LONGLONG, 6) + ms["USHORT"][SCALAR].make_empty(DT.USHORT, 7) + ms["UINT"][SCALAR].make_empty(DT.UINT, 8) + ms["ULONG"][SCALAR].make_empty(DT.ULONG, 9) + ms["ULONGLONG"][SCALAR].make_empty(DT.ULONGLONG, 10) + ms["FLOAT"][SCALAR].make_empty(DT.FLOAT, 11) + ms["DOUBLE"][SCALAR].make_empty(DT.DOUBLE, 12) + ms["LONG_DOUBLE"][SCALAR].make_empty(DT.LONG_DOUBLE, 13) + + if found_numpy: + ms["int16"][SCALAR].make_empty(np.dtype("int16"), 14) + ms["int32"][SCALAR].make_empty(np.dtype("int32"), 15) + ms["int64"][SCALAR].make_empty(np.dtype("int64"), 16) + ms["uint16"][SCALAR].make_empty(np.dtype("uint16"), 17) + ms["uint32"][SCALAR].make_empty(np.dtype("uint32"), 18) + ms["uint64"][SCALAR].make_empty(np.dtype("uint64"), 19) + ms["single"][SCALAR].make_empty(np.dtype("single"), 20) + ms["np_double"][SCALAR].make_empty(np.dtype("double"), 21) + + # flush and close file + del series + + # read back + series = io.Series( + "unittest_py_empty_API." + file_ending, + io.Access_Type.read_only + ) + + ms = series.iterations[0].meshes + + self.assertEqual( + ms["CHAR"][SCALAR].shape, + [0 for _ in range(1)] + ) + self.assertEqual( + ms["UCHAR"][SCALAR].shape, + [0 for _ in range(2)] + ) + self.assertEqual( + ms["SHORT"][SCALAR].shape, + [0 for _ in range(3)] + ) + self.assertEqual( + ms["INT"][SCALAR].shape, + [0 for _ in range(4)] + ) + self.assertEqual( + ms["LONG"][SCALAR].shape, + [0 for _ in range(5)] + ) + self.assertEqual( + ms["LONGLONG"][SCALAR].shape, + [0 for _ in range(6)] + ) + self.assertEqual( + ms["USHORT"][SCALAR].shape, + [0 for _ in range(7)] + ) + self.assertEqual( + ms["UINT"][SCALAR].shape, + [0 for _ in range(8)] + ) + self.assertEqual( + ms["ULONG"][SCALAR].shape, + [0 for _ in range(9)] + ) + self.assertEqual( + ms["ULONGLONG"][SCALAR].shape, + [0 for _ in range(10)] + ) + self.assertEqual( + ms["FLOAT"][SCALAR].shape, + [0 for _ in range(11)] + ) + self.assertEqual( + ms["DOUBLE"][SCALAR].shape, + [0 for _ in range(12)] + ) + self.assertEqual( + ms["LONG_DOUBLE"][SCALAR].shape, + [0 for _ in range(13)] + ) + + if found_numpy: + self.assertEqual( + ms["int16"][SCALAR].shape, + [0 for _ in range(14)] + ) + self.assertEqual( + ms["int32"][SCALAR].shape, + [0 for _ in range(15)] + ) + self.assertEqual( + ms["int64"][SCALAR].shape, + [0 for _ in range(16)] + ) + self.assertEqual( + ms["uint16"][SCALAR].shape, + [0 for _ in range(17)] + ) + self.assertEqual( + ms["uint32"][SCALAR].shape, + [0 for _ in range(18)] + ) + self.assertEqual( + ms["uint64"][SCALAR].shape, + [0 for _ in range(19)] + ) + self.assertEqual( + ms["single"][SCALAR].shape, + [0 for _ in range(20)] + ) + self.assertEqual( + ms["np_double"][SCALAR].shape, + [0 for _ in range(21)] + ) + + # test datatypes for fixed-sized types only + if found_numpy: + self.assertTrue(ms["int16"][SCALAR].dtype == np.dtype("int16")) + self.assertTrue(ms["int32"][SCALAR].dtype == np.dtype("int32")) + self.assertTrue(ms["int64"][SCALAR].dtype == np.dtype("int64")) + self.assertTrue(ms["uint16"][SCALAR].dtype == np.dtype("uint16")) + self.assertTrue(ms["uint32"][SCALAR].dtype == np.dtype("uint32")) + self.assertTrue(ms["uint64"][SCALAR].dtype == np.dtype("uint64")) + self.assertTrue(ms["single"][SCALAR].dtype == np.dtype("single")) + self.assertTrue( + ms["np_double"][SCALAR].dtype == np.dtype("double")) + + def testEmptyRecords(self): + backend_filesupport = { + 'json': 'json', + 'hdf5': 'h5', + 'adios1': 'bp', + 'adios2': 'bp' + } + for b in io.variants: + if io.variants[b] is True and b in backend_filesupport: + self.makeEmptyRoundTrip(backend_filesupport[b]) + def testData(self): """ Test IO on data containing particles and meshes."""