diff --git a/include/openPMD/RecordComponent.hpp b/include/openPMD/RecordComponent.hpp index 295baa82a3..0337d4ee01 100644 --- a/include/openPMD/RecordComponent.hpp +++ b/include/openPMD/RecordComponent.hpp @@ -108,6 +108,17 @@ class RecordComponent : public BaseRecordComponent template< typename T > RecordComponent& makeConstant(T); + /** + * Create a dataset with zero extent in each dimension. + * Implemented by creating a constant record component with + * a dummy value (default constructor of the respective datatype). + * @param dimensions The number of dimensions. Must be greater than + * zero. + * @return A reference to this RecordComponent. + */ + template< typename T > + RecordComponent& makeEmpty( uint8_t dimensions ); + /** Load and allocate a chunk of data * * Set offset to {0u} and extent to {-1u} for full selection. @@ -155,10 +166,19 @@ class RecordComponent : public BaseRecordComponent std::shared_ptr< std::queue< IOTask > > m_chunks; std::shared_ptr< Attribute > m_constantValue; + std::shared_ptr< bool > m_isEmpty = std::make_shared< bool >( false ); private: void flush(std::string const&); virtual void read(); + + /** + * Internal method to be called by all methods that create an empty dataset. + * + * @param The dataset description. Must have nonzero dimensions. + * @return Reference to this RecordComponent instance. + */ + RecordComponent& makeEmpty( Dataset ); }; // RecordComponent @@ -174,6 +194,15 @@ RecordComponent::makeConstant(T value) return *this; } +template< typename T > +inline RecordComponent& +RecordComponent::makeEmpty( uint8_t dimensions ) +{ + return makeEmpty( Dataset( + determineDatatype< T >(), + Extent( dimensions, 0 ) ) ); +} + template< typename T > inline std::shared_ptr< T > RecordComponent::loadChunk(Offset o, Extent e, double targetUnitSI) @@ -283,9 +312,11 @@ inline void RecordComponent::storeChunk(std::shared_ptr data, Offset o, Extent e) { if( *m_isConstant ) - throw std::runtime_error("Chunks can not be written for a constant RecordComponent."); + throw std::runtime_error("Chunks cannot be written for a constant RecordComponent."); + if( *m_isEmpty ) + throw std::runtime_error("Chunks cannot be written for an empty RecordComponent."); if( !data ) - throw std::runtime_error("Unallocated pointer passed during chunk store."); +throw std::runtime_error("Unallocated pointer passed during chunk store."); Datatype dtype = determineDatatype(data); if( dtype != getDatatype() ) { diff --git a/include/openPMD/backend/BaseRecordComponent.hpp b/include/openPMD/backend/BaseRecordComponent.hpp index 8fdfdc2e95..2c7777284c 100644 --- a/include/openPMD/backend/BaseRecordComponent.hpp +++ b/include/openPMD/backend/BaseRecordComponent.hpp @@ -61,4 +61,34 @@ class BaseRecordComponent : public Attributable std::shared_ptr< Dataset > m_dataset; std::shared_ptr< bool > m_isConstant; }; // BaseRecordComponent + +namespace detail +{ +/** + * Functor template to be used in combination with "()" + * to set a default value for constant record components via the + * respective type's default constructor. + * Used to implement empty datasets in subclasses of BaseRecordComponent + * (currently RecordComponent). + * @param rc + */ +template< typename RecordComponent_T > +struct DefaultValue +{ + template< typename T > + void + operator()( RecordComponent_T & rc ) + { + rc.makeConstant( T() ); + } + + template< unsigned n, typename... Args > + void + operator()( Args &&... ) + { + throw std::runtime_error( + "makeEmpty: Datatype not supported by openPMD." ); + } +}; +} // namespace detail } // namespace openPMD diff --git a/src/RecordComponent.cpp b/src/RecordComponent.cpp index 8e5282f2eb..d180e83901 100644 --- a/src/RecordComponent.cpp +++ b/src/RecordComponent.cpp @@ -46,16 +46,19 @@ RecordComponent::setUnitSI(double usi) return *this; } -RecordComponent& -RecordComponent::resetDataset(Dataset d) +RecordComponent & +RecordComponent::resetDataset( Dataset d ) { if( written ) - throw std::runtime_error("A Records Dataset can not (yet) be changed after it has been written."); + throw std::runtime_error( "A record's Dataset cannot (yet) be changed " + "after it has been written." ); if( d.extent.empty() ) throw std::runtime_error("Dataset extent must be at least 1D."); - if( std::any_of(d.extent.begin(), d.extent.end(), - [](Extent::value_type const& i) { return i == 0u; }) ) - throw std::runtime_error("Dataset extent must not be zero in any dimension."); + if( std::any_of( + d.extent.begin(), + d.extent.end(), + []( Extent::value_type const & i ) { return i == 0u; } ) ) + return makeEmpty( d ); *m_dataset = d; dirty = true; @@ -74,6 +77,27 @@ RecordComponent::getExtent() const return m_dataset->extent; } +RecordComponent& +RecordComponent::makeEmpty( Dataset d ) +{ + if( written ) + throw std::runtime_error( + "A RecordComponent cannot (yet) be made" + " empty after it has been written."); + if( d.extent.size() == 0 ) + throw std::runtime_error("Dataset extent must be at least 1D."); + + *m_isEmpty = true; + *m_dataset = d; + dirty = true; + static detail::DefaultValue< RecordComponent > dv; + switchType( + d.dtype, + dv, + *this ); + return *this; +} + void RecordComponent::flush(std::string const& name) { diff --git a/test/CoreTest.cpp b/test/CoreTest.cpp index ed9a17959b..e9ac792090 100644 --- a/test/CoreTest.cpp +++ b/test/CoreTest.cpp @@ -677,15 +677,10 @@ TEST_CASE( "zero_extent_component", "[core]" ) Series o = Series("./new_openpmd_output", AccessType::CREATE); auto E_x = o.iterations[1].meshes["E"]["x"]; - E_x.setComment("Datasets with zero extent in any dimension are not allowed."); - REQUIRE_THROWS_WITH(E_x.resetDataset(Dataset(Datatype::LONG, {0})), - Catch::Equals("Dataset extent must not be zero in any dimension.")); - REQUIRE_THROWS_WITH(E_x.resetDataset(Dataset(Datatype::LONG, {1, 0})), - Catch::Equals("Dataset extent must not be zero in any dimension.")); - REQUIRE_THROWS_WITH(E_x.resetDataset(Dataset(Datatype::LONG, {0, 1})), - Catch::Equals("Dataset extent must not be zero in any dimension.")); E_x.setComment("Datasets must contain dimensions."); REQUIRE_THROWS_WITH(E_x.resetDataset(Dataset(Datatype::LONG, {})), Catch::Equals("Dataset extent must be at least 1D.")); + REQUIRE_THROWS_WITH(E_x.makeEmpty(0), + Catch::Equals("Dataset extent must be at least 1D.")); E_x.resetDataset(Dataset(Datatype::DOUBLE, {1})); -} +} \ No newline at end of file diff --git a/test/SerialIOTest.cpp b/test/SerialIOTest.cpp index efbb81fead..b681fd0364 100644 --- a/test/SerialIOTest.cpp +++ b/test/SerialIOTest.cpp @@ -37,6 +37,70 @@ std::vector> getBackends() { auto const backends = getBackends(); +inline +void +empty_dataset_test( std::string file_ending ) +{ + { + Series series( + "../samples/empty_datasets." + file_ending, AccessType::CREATE ); + + auto makeEmpty_dim_7_int = + 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_resetDataset_dim3 = + series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_resetDataset_dim3" ]; + auto makeEmpty_resetDataset_dim3_notallzero = + series.iterations[ 1 ] + .meshes[ "rho" ][ "makeEmpty_resetDataset_dim3_notallzero" ]; + makeEmpty_dim_7_int.makeEmpty< int >( 7 ); + makeEmpty_dim_7_long.makeEmpty< long >( 7 ); + makeEmpty_resetDataset_dim3.resetDataset( + Dataset( Datatype::LONG, Extent( 3, 0 ) ) ); + makeEmpty_resetDataset_dim3_notallzero.resetDataset( + Dataset( Datatype::LONG_DOUBLE, Extent{ 1, 2, 0 } ) ); + series.flush(); + + } + { + Series series( + "../samples/empty_datasets." + file_ending, AccessType::READ_ONLY ); + auto makeEmpty_dim_7_int = + 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_resetDataset_dim3 = + series.iterations[ 1 ].meshes[ "rho" ][ "makeEmpty_resetDataset_dim3" ]; + auto makeEmpty_resetDataset_dim3_notallzero = + series.iterations[ 1 ] + .meshes[ "rho" ][ "makeEmpty_resetDataset_dim3_notallzero" ]; + REQUIRE(makeEmpty_dim_7_int.getDimensionality() == 7); + REQUIRE(makeEmpty_dim_7_int.getExtent() == Extent(7, 0)); + REQUIRE(isSame(makeEmpty_dim_7_int.getDatatype(), determineDatatype< int >())); + + REQUIRE(makeEmpty_dim_7_long.getDimensionality() == 7); + REQUIRE(makeEmpty_dim_7_long.getExtent() == Extent(7, 0)); + REQUIRE(isSame(makeEmpty_dim_7_long.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)); + + REQUIRE(makeEmpty_resetDataset_dim3_notallzero.getDimensionality() == 3); + REQUIRE(makeEmpty_resetDataset_dim3_notallzero.getExtent() == Extent{1,2,0}); + REQUIRE(isSame(makeEmpty_resetDataset_dim3_notallzero.getDatatype(), Datatype::LONG_DOUBLE)); + } +} + +TEST_CASE( "empty_dataset_test", "[serial]" ) +{ + for (auto const & t: backends) + { + empty_dataset_test(std::get<0>(t)); + } +} + inline void constant_scalar(std::string file_ending) { @@ -187,9 +251,9 @@ TEST_CASE( "flush_without_position_positionOffset", "[serial]" ) []( float * ptr ) { delete[] ptr; } ), { 0, 0 }, { 2, 2 } ); - + s.flush(); - + for( auto const & key : { "position", "positionOffset" } ) { for( auto const & dim : { "x", "y", "z" } )