From 9b9240b7e42167031ba85934ed8a4e336dd0f9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Thu, 5 Oct 2023 11:42:47 +0200 Subject: [PATCH 01/22] Implement per-dimension gridUnitSI --- include/openPMD/Mesh.hpp | 30 ++++++++++++++ src/Mesh.cpp | 81 ++++++++++++++++++++++++++++++++----- src/binding/python/Mesh.cpp | 4 ++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index 53274ac7d4..3031d9794e 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -184,6 +184,11 @@ class Mesh : public BaseRecord * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from * simulation units to SI units. * + * Note that this API follows the legacy (openPMD 1.*) definition + * of gridUnitSI. + * In order to specify the gridUnitSI per dimension, + * use `setGridUnitSIPerDimension()`. + * * @param gridUnitSI unit-conversion factor to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from * simulation units to SI units. @@ -191,6 +196,31 @@ class Mesh : public BaseRecord */ Mesh &setGridUnitSI(double gridUnitSI); + /** + * @return A vector of the gridUnitSI per grid dimension as defined + * by the axisLabels. If the gridUnitSI is defined as a scalar + * (legacy openPMD), the dimensionality is determined and a vector of + * `dimensionality` times the scalar vector is returned. + */ + std::vector gridUnitSIPerDimension() const; + + /** Set the unit-conversion factor per dimension to multiply each value in + * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from + * simulation units to SI units. + * + * Note that this is a feature of openPMD 2.0. + * The legacy behavior (a scalar gridUnitSI) is implemented + * by `setGridUnitSI()`. + * + * @param gridUnitSI unit-conversion factor to multiply each value in + * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from + * simulation units to SI units. + * + * @return Reference to modified mesh. + */ + + Mesh &setGridUnitSIPerDimension(std::vector gridUnitSI); + /** Set the powers of the 7 base measures characterizing the record's unit * in SI. * diff --git a/src/Mesh.cpp b/src/Mesh.cpp index f977bbe905..9c481ddbf5 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -21,8 +21,10 @@ #include "openPMD/Mesh.hpp" #include "openPMD/Error.hpp" #include "openPMD/Series.hpp" +#include "openPMD/ThrowError.hpp" #include "openPMD/auxiliary/DerefDynamicCast.hpp" #include "openPMD/auxiliary/StringManip.hpp" +#include "openPMD/backend/Attribute.hpp" #include "openPMD/backend/Writable.hpp" #include @@ -185,6 +187,47 @@ Mesh &Mesh::setGridUnitSI(double gusi) return *this; } +std::vector Mesh::gridUnitSIPerDimension() const +{ + Attribute rawAttribute = getAttribute("gridUnitSI"); + if (isVector(rawAttribute.dtype)) + { + return rawAttribute.get>(); + } + else + { + double scalarValue = rawAttribute.get(); + uint64_t dimensionality = [this]() -> uint64_t { + try + { + return axisLabels().size(); + } + catch (no_such_attribute_error const &) + { + // no-op, continue with fallback below + } + + // maybe we have record components and can ask them + if (auto it = this->begin(); it != this->end()) + { + return it->second.getDimensionality(); + } + /* + * Since some backends cannot distinguish between vector and + * scalar values, the most likely answer here is 1. + */ + return 1; + }(); + return std::vector(dimensionality, scalarValue); + } +} + +Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) +{ + setAttribute("gridUnitSI", std::move(gridUnitSI)); + return *this; +} + Mesh &Mesh::setUnitDimension(std::map const &udim) { if (!udim.empty()) @@ -385,17 +428,35 @@ void Mesh::read() aRead.name = "gridUnitSI"; IOHandler()->enqueue(IOTask(this, aRead)); IOHandler()->flush(internal::defaultFlushParams); - if (auto val = Attribute(*aRead.resource).getOptional(); - val.has_value()) - setGridUnitSI(val.value()); + if (isVector(*aRead.dtype)) + { + if (auto val = + Attribute(*aRead.resource).getOptional>(); + val.has_value()) + setGridUnitSIPerDimension(val.value()); + else + throw error::ReadError( + error::AffectedObject::Attribute, + error::Reason::UnexpectedContent, + {}, + "Unexpected Attribute datatype for 'gridUnitSI' " + "(expected vector of double, found " + + datatypeToString(Attribute(*aRead.resource).dtype) + ")"); + } else - throw error::ReadError( - error::AffectedObject::Attribute, - error::Reason::UnexpectedContent, - {}, - "Unexpected Attribute datatype for 'gridUnitSI' (expected double, " - "found " + - datatypeToString(Attribute(*aRead.resource).dtype) + ")"); + { + if (auto val = Attribute(*aRead.resource).getOptional(); + val.has_value()) + setGridUnitSI(val.value()); + else + throw error::ReadError( + error::AffectedObject::Attribute, + error::Reason::UnexpectedContent, + {}, + "Unexpected Attribute datatype for 'gridUnitSI' " + "(expected double, found " + + datatypeToString(Attribute(*aRead.resource).dtype) + ")"); + } if (scalar()) { diff --git a/src/binding/python/Mesh.cpp b/src/binding/python/Mesh.cpp index 9d53e15591..cc7a42fce4 100644 --- a/src/binding/python/Mesh.cpp +++ b/src/binding/python/Mesh.cpp @@ -96,6 +96,10 @@ void init_Mesh(py::module &m) &Mesh::gridGlobalOffset, &Mesh::setGridGlobalOffset) .def_property("grid_unit_SI", &Mesh::gridUnitSI, &Mesh::setGridUnitSI) + .def_property( + "grid_unit_SI_per_dimension", + &Mesh::gridUnitSIPerDimension, + &Mesh::setGridUnitSIPerDimension) .def_property( "time_offset", &Mesh::timeOffset, From ee7677491e42a35bd8210cb11ea084892095da56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Thu, 5 Oct 2023 12:57:29 +0200 Subject: [PATCH 02/22] Add gridUnitDimension --- include/openPMD/Mesh.hpp | 32 +++++++++- src/Mesh.cpp | 123 +++++++++++++++++++++++++++++------- src/binding/python/Mesh.cpp | 5 ++ 3 files changed, 135 insertions(+), 25 deletions(-) diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index 3031d9794e..e572f6efc7 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -155,7 +155,7 @@ class Mesh : public BaseRecord */ template < typename T, - typename = std::enable_if_t::value> > + typename = std::enable_if_t::value>> Mesh &setGridSpacing(std::vector const &gridSpacing); /** @@ -231,6 +231,34 @@ class Mesh : public BaseRecord Mesh & setUnitDimension(std::map const &unitDimension); + /** + * @brief Set the unitDimension for each axis of the current grid. + * + * @param gridUnitDimension A vector of the unitDimensions for each + * axis of the grid in the order of the axisLabels. + + * Behavior note: This is an updating method, meaning that an SI unit that + * has been defined before and is in the next call not explicitly set + * in the `std::map` will keep its previous value. + * + * @return Reference to modified mesh. + */ + Mesh &setGridUnitDimension( + std::vector> const & gridUnitDimension); + + /** + * @brief Return the physical dimensions of the mesh axes. + + * If the attribute is not defined, the axes are assumed to be spatial + * and the return value will be according to this assumption. + * If the attribute is defined, the dimensionality of the return value is + * not checked against the dimensionality of the mesh. + * + * @return A vector of arrays, each array representing the SI unit of one + * mesh axis. + */ + std::vector> gridUnitDimension() const; + /** * @tparam T Floating point type of user-selected precision (e.g. float, * double). @@ -252,7 +280,7 @@ class Mesh : public BaseRecord */ template < typename T, - typename = std::enable_if_t::value> > + typename = std::enable_if_t::value>> Mesh &setTimeOffset(T timeOffset); private: diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 9c481ddbf5..8196b86d9e 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -187,6 +187,32 @@ Mesh &Mesh::setGridUnitSI(double gusi) return *this; } +namespace +{ + uint64_t retrieveMeshDimensionality(Mesh const &m) + { + try + { + return m.axisLabels().size(); + } + catch (no_such_attribute_error const &) + { + // no-op, continue with fallback below + } + + // maybe we have record components and can ask them + if (auto it = m.begin(); it != m.end()) + { + return it->second.getDimensionality(); + } + /* + * Since some backends cannot distinguish between vector and + * scalar values, the most likely answer here is 1. + */ + return 1; + } +} // namespace + std::vector Mesh::gridUnitSIPerDimension() const { Attribute rawAttribute = getAttribute("gridUnitSI"); @@ -197,27 +223,7 @@ std::vector Mesh::gridUnitSIPerDimension() const else { double scalarValue = rawAttribute.get(); - uint64_t dimensionality = [this]() -> uint64_t { - try - { - return axisLabels().size(); - } - catch (no_such_attribute_error const &) - { - // no-op, continue with fallback below - } - - // maybe we have record components and can ask them - if (auto it = this->begin(); it != this->end()) - { - return it->second.getDimensionality(); - } - /* - * Since some backends cannot distinguish between vector and - * scalar values, the most likely answer here is 1. - */ - return 1; - }(); + uint64_t dimensionality = retrieveMeshDimensionality(*this); return std::vector(dimensionality, scalarValue); } } @@ -228,18 +234,89 @@ Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) return *this; } +namespace +{ + template + void fromMapOfUnitDimension( + RandomAccessIterator it, std::map const &udim) + { + for (auto [unit, exponent] : udim) + { + *(it + static_cast(unit)) = exponent; + } + } +} // namespace + Mesh &Mesh::setUnitDimension(std::map const &udim) { if (!udim.empty()) { std::array tmpUnitDimension = this->unitDimension(); - for (auto const &entry : udim) - tmpUnitDimension[static_cast(entry.first)] = entry.second; + fromMapOfUnitDimension(tmpUnitDimension.begin(), udim); setAttribute("unitDimension", tmpUnitDimension); } return *this; } +Mesh &Mesh::setGridUnitDimension( + std::vector> const &udims) +{ + auto rawGridUnitDimension = [this]() { + try + { + return this->getAttribute("gridUnitDimension") + .get>(); + } + catch (no_such_attribute_error const &) + { + return std::vector(); + } + }(); + rawGridUnitDimension.resize(7 * udims.size()); + auto cursor = rawGridUnitDimension.begin(); + for (auto const &udim : udims) + { + fromMapOfUnitDimension(cursor, udim); + cursor += 7; + } + setAttribute("gridUnitDimension", rawGridUnitDimension); + return *this; +} + +std::vector> Mesh::gridUnitDimension() const +{ + if (containsAttribute("gridUnitDimension")) + { + std::vector rawRes = + getAttribute("gridUnitDimension").get>(); + if (rawRes.size() % 7 != 0) + { + throw error::ReadError( + error::AffectedObject::Attribute, + error::Reason::UnexpectedContent, + std::nullopt, + "[Mesh::gridUnitDimension()] `gridUnitDimension` attribute " + "must have a length equal to a multiple of 7."); + } + std::vector> res(rawRes.size() / 7); + for (size_t dim = 0; dim < res.size(); ++dim) + { + std::copy_n(rawRes.begin() + dim * 7, 7, res.at(dim).begin()); + } + return res; + } + else + { + // gridUnitSI is an optional attribute + // if it is missing, the mesh is interpreted as spatial + std::array spatialMesh; + fromMapOfUnitDimension(spatialMesh.begin(), {{UnitDimension::L, 1}}); + auto dim = retrieveMeshDimensionality(*this); + std::vector> res(dim, spatialMesh); + return res; + } +} + template Mesh &Mesh::setTimeOffset(T to) { diff --git a/src/binding/python/Mesh.cpp b/src/binding/python/Mesh.cpp index cc7a42fce4..25daed905e 100644 --- a/src/binding/python/Mesh.cpp +++ b/src/binding/python/Mesh.cpp @@ -61,6 +61,11 @@ void init_Mesh(py::module &m) &Mesh::setUnitDimension, python::doc_unit_dimension) + .def_property( + "grid_unit_dimension", + &Mesh::gridUnitDimension, + &Mesh::setGridUnitDimension) + .def_property( "geometry", &Mesh::geometry, From bd747f42dd667436ff610a44e7766e0f0b4654e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Thu, 5 Oct 2023 16:58:20 +0200 Subject: [PATCH 03/22] Add an alias API + some bit of testing --- include/openPMD/Mesh.hpp | 25 +++++++++++++++++++++---- src/Mesh.cpp | 5 +++++ src/binding/python/Mesh.cpp | 9 +++++++-- test/SerialIOTest.cpp | 19 +++++++++++++++++-- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index e572f6efc7..edfb86680f 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -187,7 +187,7 @@ class Mesh : public BaseRecord * Note that this API follows the legacy (openPMD 1.*) definition * of gridUnitSI. * In order to specify the gridUnitSI per dimension, - * use `setGridUnitSIPerDimension()`. + * use the vector overload or `setGridUnitSIPerDimension()`. * * @param gridUnitSI unit-conversion factor to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from @@ -196,6 +196,22 @@ class Mesh : public BaseRecord */ Mesh &setGridUnitSI(double gridUnitSI); + /** Set the unit-conversion factor per dimension to multiply each value in + * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from + * simulation units to SI units. + * + * Note that this is a feature of openPMD 2.0. + * The legacy behavior (a scalar gridUnitSI) is implemented + * by `setGridUnitSI(double)`. + * + * @param gridUnitSI unit-conversion factor to multiply each value in + * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from + * simulation units to SI units. + * + * @return Reference to modified mesh. + */ + Mesh &setGridUnitSI(std::vector gridUnitSI); + /** * @return A vector of the gridUnitSI per grid dimension as defined * by the axisLabels. If the gridUnitSI is defined as a scalar @@ -204,13 +220,15 @@ class Mesh : public BaseRecord */ std::vector gridUnitSIPerDimension() const; - /** Set the unit-conversion factor per dimension to multiply each value in + /** Alias for `setGridUnitSI(std::vector)`. + * + * Set the unit-conversion factor per dimension to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from * simulation units to SI units. * * Note that this is a feature of openPMD 2.0. * The legacy behavior (a scalar gridUnitSI) is implemented - * by `setGridUnitSI()`. + * by `setGridUnitSI(double)`. * * @param gridUnitSI unit-conversion factor to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from @@ -218,7 +236,6 @@ class Mesh : public BaseRecord * * @return Reference to modified mesh. */ - Mesh &setGridUnitSIPerDimension(std::vector gridUnitSI); /** Set the powers of the 7 base measures characterizing the record's unit diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 8196b86d9e..28390bddf7 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -187,6 +187,11 @@ Mesh &Mesh::setGridUnitSI(double gusi) return *this; } +Mesh &Mesh::setGridUnitSI(std::vector gusi) +{ + return setGridUnitSIPerDimension(std::move(gusi)); +} + namespace { uint64_t retrieveMeshDimensionality(Mesh const &m) diff --git a/src/binding/python/Mesh.cpp b/src/binding/python/Mesh.cpp index 25daed905e..9ec65e7f53 100644 --- a/src/binding/python/Mesh.cpp +++ b/src/binding/python/Mesh.cpp @@ -100,7 +100,10 @@ void init_Mesh(py::module &m) "grid_global_offset", &Mesh::gridGlobalOffset, &Mesh::setGridGlobalOffset) - .def_property("grid_unit_SI", &Mesh::gridUnitSI, &Mesh::setGridUnitSI) + .def_property( + "grid_unit_SI", + &Mesh::gridUnitSI, + py::overload_cast(&Mesh::setGridUnitSI)) .def_property( "grid_unit_SI_per_dimension", &Mesh::gridUnitSIPerDimension, @@ -122,7 +125,9 @@ void init_Mesh(py::module &m) .def("set_grid_spacing", &Mesh::setGridSpacing) .def("set_grid_spacing", &Mesh::setGridSpacing) .def("set_grid_global_offset", &Mesh::setGridGlobalOffset) - .def("set_grid_unit_SI", &Mesh::setGridUnitSI); + .def( + "set_grid_unit_SI", + py::overload_cast(&Mesh::setGridUnitSI)); add_pickle( cl, [](openPMD::Series series, std::vector const &group) { uint64_t const n_it = std::stoull(group.at(1)); diff --git a/test/SerialIOTest.cpp b/test/SerialIOTest.cpp index 3ff3f7572c..b77b21e5c2 100644 --- a/test/SerialIOTest.cpp +++ b/test/SerialIOTest.cpp @@ -2,6 +2,7 @@ #include "openPMD/ChunkInfo_internal.hpp" #include "openPMD/Datatype.hpp" #include "openPMD/IO/Access.hpp" +#include "openPMD/UnitDimension.hpp" #if openPMD_USE_INVASIVE_TESTS #define OPENPMD_private public: #define OPENPMD_protected public: @@ -936,12 +937,20 @@ inline void constant_scalar(std::string const &file_ending) // store a number of predefined attributes in E Mesh &E_mesh = s.iterations[1].meshes["E"]; + // test that these can be defined successively + E_mesh.setGridUnitDimension({{{UnitDimension::L, 1}}, {}, {}}); + E_mesh.setGridUnitDimension( + {{}, {{UnitDimension::L, 1}}, {{UnitDimension::L, 1}}}); + // let's modify the last dimension for the test's purpose now + E_mesh.setGridUnitDimension({{}, {}, {{UnitDimension::M, 1}}}); + // should now all be [1,0,0,0,0,0,0], except the last which should be + // [1,1,0,0,0,0,0] E_mesh.setGeometry(geometry); E_mesh.setGeometryParameters(geometryParameters); E_mesh.setDataOrder(dataOrder); E_mesh.setGridSpacing(gridSpacing); E_mesh.setGridGlobalOffset(gridGlobalOffset); - E_mesh.setGridUnitSI(gridUnitSI); + E_mesh.setGridUnitSI(std::vector(3, gridUnitSI)); E_mesh.setAxisLabels(axisLabels); E_mesh.setUnitDimension(unitDimensions); E_mesh.setTimeOffset(timeOffset); @@ -1105,12 +1114,18 @@ inline void constant_scalar(std::string const &file_ending) Extent{3, 2, 1}); Mesh &E_mesh = s.iterations[1].meshes["E"]; + REQUIRE( + E_mesh.gridUnitDimension() == + std::vector{ + std::array{1., 0., 0., 0., 0, .0, 0.}, + std::array{1., 0., 0., 0., 0, .0, 0.}, + std::array{1., 1., 0., 0., 0, .0, 0.}}); REQUIRE(E_mesh.geometry() == geometry); REQUIRE(E_mesh.geometryParameters() == geometryParameters); REQUIRE(E_mesh.dataOrder() == dataOrder); REQUIRE(E_mesh.gridSpacing() == gridSpacing); REQUIRE(E_mesh.gridGlobalOffset() == gridGlobalOffset); - REQUIRE(E_mesh.gridUnitSI() == gridUnitSI); + REQUIRE(E_mesh.gridUnitSIPerDimension() == std::vector(3, gridUnitSI)); REQUIRE(E_mesh.axisLabels() == axisLabels); // REQUIRE( E_mesh.unitDimension() == unitDimensions ); REQUIRE(E_mesh.timeOffset() == timeOffset); From 272fbdc235245ddf6ce2a600789fbf431315c820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Fri, 6 Oct 2023 14:45:37 +0200 Subject: [PATCH 04/22] Add upgrade notice for old API call We cannot upgrade users silently in this place --- .../openPMD/auxiliary/DerefDynamicCast.hpp | 25 ++++++++++++++- include/openPMD/backend/Attributable.hpp | 3 ++ src/Mesh.cpp | 32 ++++++++++++------- src/backend/Attributable.cpp | 30 +++++++++++++++-- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/include/openPMD/auxiliary/DerefDynamicCast.hpp b/include/openPMD/auxiliary/DerefDynamicCast.hpp index 1620334ea0..6b2674ae9e 100644 --- a/include/openPMD/auxiliary/DerefDynamicCast.hpp +++ b/include/openPMD/auxiliary/DerefDynamicCast.hpp @@ -20,7 +20,8 @@ */ #pragma once -#include +#include +#include namespace openPMD { @@ -47,5 +48,27 @@ namespace auxiliary return *tmp_ptr; } + /** Returns a value reference stored in a dynamically casted pointer + * + * Safe version of *dynamic_cast< New_Type* >( some_ptr ); This function + * will throw as dynamic_cast and will furthermore throw if the result + * of the dynamic_cast is a nullptr. + * + * @tparam New_Type new type to cast to + * @tparam Old_Type old type to cast from + * @param[in] ptr and input pointer type + * @return value reference of a dereferenced, dynamically casted ptr to + * New_Type* + */ + template + inline std::optional + dynamic_cast_optional(Old_Type *ptr) noexcept + { + auto const tmp_ptr = dynamic_cast(ptr); + if (tmp_ptr == nullptr) + return std::nullopt; + return {tmp_ptr}; + } + } // namespace auxiliary } // namespace openPMD diff --git a/include/openPMD/backend/Attributable.hpp b/include/openPMD/backend/Attributable.hpp index 850d3bf35c..85d3712277 100644 --- a/include/openPMD/backend/Attributable.hpp +++ b/include/openPMD/backend/Attributable.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -332,6 +333,8 @@ class Attributable OPENPMD_protected // clang-format on + std::optional retrieveSeries_optional() const; + Series retrieveSeries() const; /** Returns the corresponding Iteration diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 28390bddf7..b0fa2aba6a 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -184,6 +184,20 @@ double Mesh::gridUnitSI() const Mesh &Mesh::setGridUnitSI(double gusi) { setAttribute("gridUnitSI", gusi); + if (auto series_opt = retrieveSeries_optional(); series_opt.has_value()) + { + if (auto version = series_opt->openPMD(); version >= "2.") + { + std::cerr << "[Mesh::setGridUnitSI] Warning: Setting a scalar " + "`gridUnitSI` in a file with openPMD version '" + + version + + "'. Consider specifying a vector instead in order to " + "specify " + "the gridUnitSI per axis (ref.: " + "https://github.com/openPMD/openPMD-standard/pull/193)." + << std::endl; + } + } return *this; } @@ -220,17 +234,7 @@ namespace std::vector Mesh::gridUnitSIPerDimension() const { - Attribute rawAttribute = getAttribute("gridUnitSI"); - if (isVector(rawAttribute.dtype)) - { - return rawAttribute.get>(); - } - else - { - double scalarValue = rawAttribute.get(); - uint64_t dimensionality = retrieveMeshDimensionality(*this); - return std::vector(dimensionality, scalarValue); - } + return getAttribute("gridUnitSI").get>(); } Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) @@ -510,7 +514,11 @@ void Mesh::read() aRead.name = "gridUnitSI"; IOHandler()->enqueue(IOTask(this, aRead)); IOHandler()->flush(internal::defaultFlushParams); - if (isVector(*aRead.dtype)) + auto series = retrieveSeries(); + /* @todo remove second if condition (currently enabled since it allows a + * sneak peek into openPMD 2.0 features) + */ + if (series.openPMD() >= "2." || isVector(*aRead.dtype)) { if (auto val = Attribute(*aRead.resource).getOptional>(); diff --git a/src/backend/Attributable.cpp b/src/backend/Attributable.cpp index 9ffbe25ee3..dd7f5a621a 100644 --- a/src/backend/Attributable.cpp +++ b/src/backend/Attributable.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -130,14 +131,39 @@ void Attributable::iterationFlush(std::string backendConfig) std::move(backendConfig)); } -Series Attributable::retrieveSeries() const +std::optional Attributable::retrieveSeries_optional() const { Writable const *findSeries = &writable(); while (findSeries->parent) { findSeries = findSeries->parent; } - return findSeries->attributable->asInternalCopyOf(); + auto maybeSeriesData = + auxiliary::dynamic_cast_optional( + findSeries->attributable); + if (!maybeSeriesData.has_value()) + { + return std::nullopt; + } + auto seriesData = *maybeSeriesData; + return seriesData->asInternalCopyOf(); +} + +Series Attributable::retrieveSeries() const +{ + if (auto maybeSeries = retrieveSeries_optional(); maybeSeries.has_value()) + { + return *maybeSeries; + } + else + { + throw std::runtime_error( + "[Attributable::retrieveSeries] Error when trying to retrieve the " + "Series object. Note: An instance of the Series object must still " + "exist when flushing. A common cause for this error is using a " + "flush call on a handle (e.g. `Iteration::seriesFlush()`) when the " + "original Series object has already gone out of scope."); + } } auto Attributable::containingIteration() const -> std::pair< From 41587cd7ad84f3dbdc4fd160dea5790ecdad932c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 9 Oct 2023 16:25:10 +0200 Subject: [PATCH 05/22] Use openPMD 2.0 standard setting --- src/Mesh.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index b0fa2aba6a..fbf54484f0 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -240,6 +240,19 @@ std::vector Mesh::gridUnitSIPerDimension() const Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) { setAttribute("gridUnitSI", std::move(gridUnitSI)); + if (auto series_opt = retrieveSeries_optional(); series_opt.has_value()) + { + if (auto version = series_opt->openPMD(); version < "2.") + { + throw error::IllegalInOpenPMDStandard( + "[Mesh::setGridUnitSI] Setting `gridUnitSI` as a vector in a " + "file with openPMD version '" + + version + + "', but per-axis specification is only supported as of " + "openPMD 2.0. Either upgrade the file to openPMD >= 2.0 " + "or specify a scalar that applies to all axes."); + } + } return *this; } @@ -515,10 +528,7 @@ void Mesh::read() IOHandler()->enqueue(IOTask(this, aRead)); IOHandler()->flush(internal::defaultFlushParams); auto series = retrieveSeries(); - /* @todo remove second if condition (currently enabled since it allows a - * sneak peek into openPMD 2.0 features) - */ - if (series.openPMD() >= "2." || isVector(*aRead.dtype)) + if (series.openPMD() >= "2.") { if (auto val = Attribute(*aRead.resource).getOptional>(); From a3c059d9a52e0bf9e5ec5552b724baa7477c5039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Wed, 11 Oct 2023 09:25:44 +0200 Subject: [PATCH 06/22] Two little fixes --- include/openPMD/auxiliary/DerefDynamicCast.hpp | 14 +++++++++++--- src/Mesh.cpp | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/include/openPMD/auxiliary/DerefDynamicCast.hpp b/include/openPMD/auxiliary/DerefDynamicCast.hpp index 6b2674ae9e..ecbf2033e1 100644 --- a/include/openPMD/auxiliary/DerefDynamicCast.hpp +++ b/include/openPMD/auxiliary/DerefDynamicCast.hpp @@ -22,6 +22,7 @@ #include #include +#include namespace openPMD { @@ -64,10 +65,17 @@ namespace auxiliary inline std::optional dynamic_cast_optional(Old_Type *ptr) noexcept { - auto const tmp_ptr = dynamic_cast(ptr); - if (tmp_ptr == nullptr) + try + { + auto const tmp_ptr = dynamic_cast(ptr); + if (tmp_ptr == nullptr) + return std::nullopt; + return {tmp_ptr}; + } + catch (std::bad_cast const &) + { return std::nullopt; - return {tmp_ptr}; + } } } // namespace auxiliary diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fbf54484f0..a6cc2842de 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -264,7 +264,7 @@ namespace { for (auto [unit, exponent] : udim) { - *(it + static_cast(unit)) = exponent; + it[static_cast(unit)] = exponent; } } } // namespace From 01b46eae696ba24a799d027d1e754a1be5d307d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 18 Dec 2023 15:24:50 +0100 Subject: [PATCH 07/22] Default value for gridUnitSI set upon flush --- include/openPMD/Mesh.hpp | 21 ++++++++++----------- src/Mesh.cpp | 13 ++++++++++++- test/CoreTest.cpp | 15 +++++++-------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index edfb86680f..dd830a5613 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -184,9 +184,8 @@ class Mesh : public BaseRecord * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from * simulation units to SI units. * - * Note that this API follows the legacy (openPMD 1.*) definition - * of gridUnitSI. - * In order to specify the gridUnitSI per dimension, + * Valid for openPMD version 1.*. + * In order to specify the gridUnitSI per dimension (openPMD 2.*), * use the vector overload or `setGridUnitSIPerDimension()`. * * @param gridUnitSI unit-conversion factor to multiply each value in @@ -196,12 +195,14 @@ class Mesh : public BaseRecord */ Mesh &setGridUnitSI(double gridUnitSI); - /** Set the unit-conversion factor per dimension to multiply each value in + /** Alias for `setGridUnitSI(std::vector)`. + * + * Set the unit-conversion factor per dimension to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from * simulation units to SI units. * - * Note that this is a feature of openPMD 2.0. - * The legacy behavior (a scalar gridUnitSI) is implemented + * Valid for openPMD 2.*. + * The legacy behavior (openPMD 1.*, a scalar gridUnitSI) is implemented * by `setGridUnitSI(double)`. * * @param gridUnitSI unit-conversion factor to multiply each value in @@ -220,14 +221,12 @@ class Mesh : public BaseRecord */ std::vector gridUnitSIPerDimension() const; - /** Alias for `setGridUnitSI(std::vector)`. - * - * Set the unit-conversion factor per dimension to multiply each value in + /* Set the unit-conversion factor per dimension to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from * simulation units to SI units. * - * Note that this is a feature of openPMD 2.0. - * The legacy behavior (a scalar gridUnitSI) is implemented + * Valid for openPMD 2.*. + * The legacy behavior (openPMD 1.*, a scalar gridUnitSI) is implemented * by `setGridUnitSI(double)`. * * @param gridUnitSI unit-conversion factor to multiply each value in diff --git a/src/Mesh.cpp b/src/Mesh.cpp index a6cc2842de..2ac6d6bb42 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -42,7 +42,6 @@ Mesh::Mesh() setAxisLabels({"x"}); // empty strings are not allowed in HDF5 setGridSpacing(std::vector{1}); setGridGlobalOffset({0}); - setGridUnitSI(1); } Mesh::Geometry Mesh::geometry() const @@ -405,6 +404,18 @@ void Mesh::flush_impl( comp.second.flush(comp.first, flushParams); } } + if (!containsAttribute("gridUnitSI")) + { + if (auto series = retrieveSeries(); series.openPMD() < "2.") + { + setGridUnitSI(1); + } + else + { + setGridUnitSIPerDimension( + std::vector(retrieveMeshDimensionality(*this), 1)); + } + } flushAttributes(flushParams); } } diff --git a/test/CoreTest.cpp b/test/CoreTest.cpp index d27d68a8c5..2c4648925d 100644 --- a/test/CoreTest.cpp +++ b/test/CoreTest.cpp @@ -528,11 +528,10 @@ TEST_CASE("mesh_constructor_test", "[core]") REQUIRE(m.gridSpacing() == gs); std::vector ggo{0}; REQUIRE(m.gridGlobalOffset() == ggo); - REQUIRE(m.gridUnitSI() == static_cast(1)); REQUIRE( m.numAttributes() == - 8); /* axisLabels, dataOrder, geometry, gridGlobalOffset, gridSpacing, - gridUnitSI, timeOffset, unitDimension */ + 7); /* axisLabels, dataOrder, geometry, gridGlobalOffset, gridSpacing, + timeOffset, unitDimension */ o.flush(); REQUIRE(m["x"].unitSI() == 1); @@ -557,22 +556,22 @@ TEST_CASE("mesh_modification_test", "[core]") m.setGeometry(Mesh::Geometry::spherical); REQUIRE(m.geometry() == Mesh::Geometry::spherical); - REQUIRE(m.numAttributes() == 8); + REQUIRE(m.numAttributes() == 7); m.setDataOrder(Mesh::DataOrder::F); REQUIRE(m.dataOrder() == Mesh::DataOrder::F); - REQUIRE(m.numAttributes() == 8); + REQUIRE(m.numAttributes() == 7); std::vector al{"z_", "y_", "x_"}; m.setAxisLabels({"z_", "y_", "x_"}); REQUIRE(m.axisLabels() == al); - REQUIRE(m.numAttributes() == 8); + REQUIRE(m.numAttributes() == 7); std::vector gs{1e-5, 2e-5, 3e-5}; m.setGridSpacing(gs); REQUIRE(m.gridSpacing() == gs); - REQUIRE(m.numAttributes() == 8); + REQUIRE(m.numAttributes() == 7); std::vector ggo{1e-10, 2e-10, 3e-10}; m.setGridGlobalOffset({1e-10, 2e-10, 3e-10}); REQUIRE(m.gridGlobalOffset() == ggo); - REQUIRE(m.numAttributes() == 8); + REQUIRE(m.numAttributes() == 7); m.setGridUnitSI(42.0); REQUIRE(m.gridUnitSI() == static_cast(42)); REQUIRE(m.numAttributes() == 8); From d183aaf60eb8c350cc71234a9d66b14270e411ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 10:44:34 +0000 Subject: [PATCH 08/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- include/openPMD/Mesh.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index dd830a5613..62d574b67a 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -260,7 +260,7 @@ class Mesh : public BaseRecord * @return Reference to modified mesh. */ Mesh &setGridUnitDimension( - std::vector> const & gridUnitDimension); + std::vector> const &gridUnitDimension); /** * @brief Return the physical dimensions of the mesh axes. From 3c0b6e9704289d794996eb89a6e67adab61f1792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Wed, 3 Jul 2024 16:21:23 +0200 Subject: [PATCH 09/22] Fallback for undefined gridUnitSI --- src/Mesh.cpp | 30 +++++++++++++++++++++++------- test/SerialIOTest.cpp | 7 ++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 2ac6d6bb42..b4e0584dcb 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -209,14 +209,10 @@ namespace { uint64_t retrieveMeshDimensionality(Mesh const &m) { - try + if (m.containsAttribute("axisLabels")) { return m.axisLabels().size(); } - catch (no_such_attribute_error const &) - { - // no-op, continue with fallback below - } // maybe we have record components and can ask them if (auto it = m.begin(); it != m.end()) @@ -233,7 +229,27 @@ namespace std::vector Mesh::gridUnitSIPerDimension() const { - return getAttribute("gridUnitSI").get>(); + if (containsAttribute("gridUnitSI")) + { + if (auto series_opt = retrieveSeries_optional(); series_opt.has_value()) + { + if (auto version = series_opt->openPMD(); version < "2.") + { + // If the openPMD version is lower than 2.0, the gridUnitSI is a + // scalar interpreted for all axes. Copy it d times. + return std::vector( + retrieveMeshDimensionality(*this), + getAttribute("gridUnitSI").get()); + } + } + return getAttribute("gridUnitSI").get>(); + } + else + { + // gridUnitSI is an optional attribute + // if it is missing, the mesh is interpreted as unscaled + return std::vector(retrieveMeshDimensionality(*this), 1.); + } } Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) @@ -328,7 +344,7 @@ std::vector> Mesh::gridUnitDimension() const } else { - // gridUnitSI is an optional attribute + // gridUnitDimension is an optional attribute // if it is missing, the mesh is interpreted as spatial std::array spatialMesh; fromMapOfUnitDimension(spatialMesh.begin(), {{UnitDimension::L, 1}}); diff --git a/test/SerialIOTest.cpp b/test/SerialIOTest.cpp index b77b21e5c2..12def16347 100644 --- a/test/SerialIOTest.cpp +++ b/test/SerialIOTest.cpp @@ -950,10 +950,15 @@ inline void constant_scalar(std::string const &file_ending) E_mesh.setDataOrder(dataOrder); E_mesh.setGridSpacing(gridSpacing); E_mesh.setGridGlobalOffset(gridGlobalOffset); - E_mesh.setGridUnitSI(std::vector(3, gridUnitSI)); E_mesh.setAxisLabels(axisLabels); E_mesh.setUnitDimension(unitDimensions); E_mesh.setTimeOffset(timeOffset); + REQUIRE( + E_mesh.gridUnitSIPerDimension() == std::vector{1., 1., 1.}); + E_mesh.setGridUnitSI(std::vector(3, gridUnitSI)); + REQUIRE( + E_mesh.gridUnitSIPerDimension() == + std::vector{gridUnitSI, gridUnitSI, gridUnitSI}); // constant scalar auto pos = From ec6f93cbcc22f6c904247421575b6e8a24da92a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Tue, 29 Oct 2024 14:56:51 +0100 Subject: [PATCH 10/22] Add conversion helpers between map- and array representations --- CMakeLists.txt | 1 + include/openPMD/Mesh.hpp | 3 +- include/openPMD/UnitDimension.hpp | 27 ++++++++++++ include/openPMD/backend/BaseRecord.hpp | 4 +- src/Mesh.cpp | 27 ++++-------- src/UnitDimension.cpp | 61 ++++++++++++++++++++++++++ test/CoreTest.cpp | 17 +++++++ 7 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 src/UnitDimension.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fc5d6d5de5..10e74dc3a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -397,6 +397,7 @@ set(CORE_SOURCE src/Record.cpp src/RecordComponent.cpp src/Series.cpp + src/UnitDimension.cpp src/version.cpp src/WriteIterations.cpp src/auxiliary/Date.cpp diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index 62d574b67a..119111ea53 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -20,6 +20,7 @@ */ #pragma once +#include "openPMD/UnitDimension.hpp" #include "openPMD/backend/Attributable.hpp" #include "openPMD/backend/BaseRecord.hpp" #include "openPMD/backend/MeshRecordComponent.hpp" @@ -273,7 +274,7 @@ class Mesh : public BaseRecord * @return A vector of arrays, each array representing the SI unit of one * mesh axis. */ - std::vector> gridUnitDimension() const; + unit_representations::AsArrays gridUnitDimension() const; /** * @tparam T Floating point type of user-selected precision (e.g. float, diff --git a/include/openPMD/UnitDimension.hpp b/include/openPMD/UnitDimension.hpp index 232a6ee1f7..b6b82d4ea3 100644 --- a/include/openPMD/UnitDimension.hpp +++ b/include/openPMD/UnitDimension.hpp @@ -20,10 +20,16 @@ */ #pragma once +#include #include +#include +#include namespace openPMD { + +using UnitDimensionExponent = double; + /** Physical dimension of a record * * Dimensional base quantities of the international system of quantities @@ -38,4 +44,25 @@ enum class UnitDimension : uint8_t N, //!< amount of substance J //!< luminous intensity }; + +namespace unit_representations +{ + using AsMap = std::map; + using AsArray = std::array; + + using AsMaps = std::vector; + using AsArrays = std::vector; + + auto asArray(AsMap const &) -> AsArray; + auto asMap(AsArray const &) -> AsMap; + + auto asArrays(AsMaps const &) -> AsArrays; + auto asMaps(AsArrays const &) -> AsMaps; +} // namespace unit_representations + +namespace auxiliary +{ + void fromMapOfUnitDimension( + double *cursor, std::map const &udim); +} // namespace auxiliary } // namespace openPMD diff --git a/include/openPMD/backend/BaseRecord.hpp b/include/openPMD/backend/BaseRecord.hpp index ba137b10db..87364b46f4 100644 --- a/include/openPMD/backend/BaseRecord.hpp +++ b/include/openPMD/backend/BaseRecord.hpp @@ -474,7 +474,7 @@ class BaseRecord * * @return powers of the 7 base measures in the order specified above */ - std::array unitDimension() const; + unit_representations::AsArray unitDimension() const; void setDatasetDefined(BaseRecordComponent::Data_t &data) override { @@ -928,7 +928,7 @@ auto BaseRecord::emplace(Args &&...args) -> std::pair } template -inline std::array BaseRecord::unitDimension() const +inline unit_representations::AsArray BaseRecord::unitDimension() const { return this->getAttribute("unitDimension") .template get>(); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index b4e0584dcb..5839da7cab 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -22,6 +22,7 @@ #include "openPMD/Error.hpp" #include "openPMD/Series.hpp" #include "openPMD/ThrowError.hpp" +#include "openPMD/UnitDimension.hpp" #include "openPMD/auxiliary/DerefDynamicCast.hpp" #include "openPMD/auxiliary/StringManip.hpp" #include "openPMD/backend/Attribute.hpp" @@ -271,25 +272,12 @@ Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) return *this; } -namespace -{ - template - void fromMapOfUnitDimension( - RandomAccessIterator it, std::map const &udim) - { - for (auto [unit, exponent] : udim) - { - it[static_cast(unit)] = exponent; - } - } -} // namespace - Mesh &Mesh::setUnitDimension(std::map const &udim) { if (!udim.empty()) { std::array tmpUnitDimension = this->unitDimension(); - fromMapOfUnitDimension(tmpUnitDimension.begin(), udim); + auxiliary::fromMapOfUnitDimension(tmpUnitDimension.begin(), udim); setAttribute("unitDimension", tmpUnitDimension); } return *this; @@ -313,14 +301,14 @@ Mesh &Mesh::setGridUnitDimension( auto cursor = rawGridUnitDimension.begin(); for (auto const &udim : udims) { - fromMapOfUnitDimension(cursor, udim); + auxiliary::fromMapOfUnitDimension(&*cursor, udim); cursor += 7; } setAttribute("gridUnitDimension", rawGridUnitDimension); return *this; } -std::vector> Mesh::gridUnitDimension() const +unit_representations::AsArrays Mesh::gridUnitDimension() const { if (containsAttribute("gridUnitDimension")) { @@ -335,7 +323,7 @@ std::vector> Mesh::gridUnitDimension() const "[Mesh::gridUnitDimension()] `gridUnitDimension` attribute " "must have a length equal to a multiple of 7."); } - std::vector> res(rawRes.size() / 7); + unit_representations::AsArrays res(rawRes.size() / 7); for (size_t dim = 0; dim < res.size(); ++dim) { std::copy_n(rawRes.begin() + dim * 7, 7, res.at(dim).begin()); @@ -347,9 +335,10 @@ std::vector> Mesh::gridUnitDimension() const // gridUnitDimension is an optional attribute // if it is missing, the mesh is interpreted as spatial std::array spatialMesh; - fromMapOfUnitDimension(spatialMesh.begin(), {{UnitDimension::L, 1}}); + auxiliary::fromMapOfUnitDimension( + spatialMesh.begin(), {{UnitDimension::L, 1}}); auto dim = retrieveMeshDimensionality(*this); - std::vector> res(dim, spatialMesh); + unit_representations::AsArrays res(dim, spatialMesh); return res; } } diff --git a/src/UnitDimension.cpp b/src/UnitDimension.cpp new file mode 100644 index 0000000000..b2d36e7b8e --- /dev/null +++ b/src/UnitDimension.cpp @@ -0,0 +1,61 @@ +#include "openPMD/UnitDimension.hpp" +#include +#include + +namespace openPMD +{ +namespace auxiliary +{ + void fromMapOfUnitDimension( + double *cursor, std::map const &udim) + { + for (auto [unit, exponent] : udim) + { + cursor[static_cast(unit)] = exponent; + } + } +} // namespace auxiliary + +namespace unit_representations +{ + auto asArray(AsMap const &udim) -> AsArray + { + AsArray res; + auxiliary::fromMapOfUnitDimension(res.begin(), udim); + return res; + } + auto asMap(AsArray const &array) -> AsMap + { + AsMap udim; + for (size_t i = 0; i < array.size(); ++i) + { + if (array[i] != 0) + { + udim[static_cast(i)] = array[i]; + } + } + return udim; + } + + auto asArrays(AsMaps const &vec) -> AsArrays + { + AsArrays res; + std::transform( + vec.begin(), + vec.end(), + std::back_inserter(res), + [](auto const &map) { return asArray(map); }); + return res; + } + auto asMaps(AsArrays const &vec) -> AsMaps + { + AsMaps res; + std::transform( + vec.begin(), + vec.end(), + std::back_inserter(res), + [](auto const &array) { return asMap(array); }); + return res; + } +} // namespace unit_representations +} // namespace openPMD diff --git a/test/CoreTest.cpp b/test/CoreTest.cpp index 2c4648925d..7c3a5c4ed9 100644 --- a/test/CoreTest.cpp +++ b/test/CoreTest.cpp @@ -1,4 +1,5 @@ // expose private and protected members for invasive testing +#include "openPMD/UnitDimension.hpp" #if openPMD_USE_INVASIVE_TESTS #define OPENPMD_private public: #define OPENPMD_protected public: @@ -1280,6 +1281,16 @@ TEST_CASE("custom_geometries", "[core]") Series write("../samples/custom_geometry.json", Access::CREATE); auto E = write.iterations[0].meshes["E"]; E.setAttribute("geometry", "other:customGeometry"); + // gridUnitDimension is technically an openPMD 2.0 addition, but since + // it's a non-breaking addition, we can also use it in openPMD 1.* + // files. However, it only really makes sense to use along with per-axis + // gridUnitSI definitions, which are in fact breaking in comparison to + // openPMD 1.*. + E.setGridUnitDimension( + {{{UnitDimension::theta, 1}}, + {{UnitDimension::M, 1}, + {UnitDimension::L, 1}, + {UnitDimension::T, 2}}}); auto E_x = E["x"]; E_x.resetDataset({Datatype::INT, {10}}); E_x.storeChunk(sampleData, {0}, {10}); @@ -1306,6 +1317,12 @@ TEST_CASE("custom_geometries", "[core]") { Series read("../samples/custom_geometry.json", Access::READ_ONLY); auto E = read.iterations[0].meshes["E"]; + auto compare = unit_representations::AsMaps{ + {{UnitDimension::theta, 1}}, + {{UnitDimension::M, 1}, + {UnitDimension::L, 1}, + {UnitDimension::T, 2}}}; + REQUIRE(unit_representations::asMaps(E.gridUnitDimension()) == compare); REQUIRE( E.getAttribute("geometry").get() == "other:customGeometry"); From 3ad4f1e933e20509deff8c0b1342751ec289b5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Wed, 30 Oct 2024 10:40:49 +0100 Subject: [PATCH 11/22] Windows fixes --- src/Mesh.cpp | 4 ++-- src/UnitDimension.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 5839da7cab..69fc6a0263 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -277,7 +277,7 @@ Mesh &Mesh::setUnitDimension(std::map const &udim) if (!udim.empty()) { std::array tmpUnitDimension = this->unitDimension(); - auxiliary::fromMapOfUnitDimension(tmpUnitDimension.begin(), udim); + auxiliary::fromMapOfUnitDimension(tmpUnitDimension.data(), udim); setAttribute("unitDimension", tmpUnitDimension); } return *this; @@ -336,7 +336,7 @@ unit_representations::AsArrays Mesh::gridUnitDimension() const // if it is missing, the mesh is interpreted as spatial std::array spatialMesh; auxiliary::fromMapOfUnitDimension( - spatialMesh.begin(), {{UnitDimension::L, 1}}); + spatialMesh.data(), {{UnitDimension::L, 1}}); auto dim = retrieveMeshDimensionality(*this); unit_representations::AsArrays res(dim, spatialMesh); return res; diff --git a/src/UnitDimension.cpp b/src/UnitDimension.cpp index b2d36e7b8e..a2420defec 100644 --- a/src/UnitDimension.cpp +++ b/src/UnitDimension.cpp @@ -21,7 +21,7 @@ namespace unit_representations auto asArray(AsMap const &udim) -> AsArray { AsArray res; - auxiliary::fromMapOfUnitDimension(res.begin(), udim); + auxiliary::fromMapOfUnitDimension(res.data(), udim); return res; } auto asMap(AsArray const &array) -> AsMap From 05183c68b29de26e402911c50b09e352a0a68821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Wed, 30 Oct 2024 12:30:24 +0100 Subject: [PATCH 12/22] Tests and bugfixes --- src/Mesh.cpp | 5 ++--- src/UnitDimension.cpp | 4 +++- src/binding/python/UnitDimension.cpp | 16 +++++++++++++++- test/python/unittest/API/APITest.py | 8 ++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 69fc6a0263..e2e239299e 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -334,9 +334,8 @@ unit_representations::AsArrays Mesh::gridUnitDimension() const { // gridUnitDimension is an optional attribute // if it is missing, the mesh is interpreted as spatial - std::array spatialMesh; - auxiliary::fromMapOfUnitDimension( - spatialMesh.data(), {{UnitDimension::L, 1}}); + auto spatialMesh = + unit_representations::asArray({{UnitDimension::L, 1}}); auto dim = retrieveMeshDimensionality(*this); unit_representations::AsArrays res(dim, spatialMesh); return res; diff --git a/src/UnitDimension.cpp b/src/UnitDimension.cpp index a2420defec..c9aa449347 100644 --- a/src/UnitDimension.cpp +++ b/src/UnitDimension.cpp @@ -20,7 +20,7 @@ namespace unit_representations { auto asArray(AsMap const &udim) -> AsArray { - AsArray res; + AsArray res{}; auxiliary::fromMapOfUnitDimension(res.data(), udim); return res; } @@ -40,6 +40,7 @@ namespace unit_representations auto asArrays(AsMaps const &vec) -> AsArrays { AsArrays res; + res.reserve(vec.size()); std::transform( vec.begin(), vec.end(), @@ -50,6 +51,7 @@ namespace unit_representations auto asMaps(AsArrays const &vec) -> AsMaps { AsMaps res; + res.reserve(vec.size()); std::transform( vec.begin(), vec.end(), diff --git a/src/binding/python/UnitDimension.cpp b/src/binding/python/UnitDimension.cpp index 6e46a6cfcf..eb32e87de1 100644 --- a/src/binding/python/UnitDimension.cpp +++ b/src/binding/python/UnitDimension.cpp @@ -31,5 +31,19 @@ void init_UnitDimension(py::module &m) .value("I", UnitDimension::I) .value("theta", UnitDimension::theta) .value("N", UnitDimension::N) - .value("J", UnitDimension::J); + .value("J", UnitDimension::J) + .def( + "as_index", + [](UnitDimension ud) -> uint8_t { + return static_cast(ud); + }) + .def( + "from_index", + [](uint8_t idx) -> UnitDimension { + return static_cast(idx); + }) + .def("as_array", &unit_representations::asArray) + .def("as_map", &unit_representations::asMap) + .def("as_arrays", &unit_representations::asArrays) + .def("as_maps", &unit_representations::asMaps); } diff --git a/test/python/unittest/API/APITest.py b/test/python/unittest/API/APITest.py index 59e6b5c97e..f828ab9795 100644 --- a/test/python/unittest/API/APITest.py +++ b/test/python/unittest/API/APITest.py @@ -98,6 +98,14 @@ def testFieldData(self): self.assertEqual(len(i.meshes), 2) for m in i.meshes: self.assertTrue(m in ["E", "rho"]) + self.assertEqual(i.meshes[m].unit_dimension, io.Unit_Dimension.as_array( + io.Unit_Dimension.as_map(i.meshes[m].unit_dimension))) + self.assertEqual(io.Unit_Dimension.as_maps(i.meshes[m].grid_unit_dimension), [ + {io.Unit_Dimension.L: 1}, {io.Unit_Dimension.L: 1}]) + self.assertEqual(io.Unit_Dimension.from_index(0), io.Unit_Dimension.L) + self.assertEqual(io.Unit_Dimension.L.as_index(), 0) + for idx in range(7): + self.assertEqual(idx, io.Unit_Dimension.from_index(idx).as_index()) # Check entries. self.assertEqual(len(i.meshes), 2) From d0476f110c3f6a4d801c108059ef9ecbf0f10bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Wed, 30 Oct 2024 15:22:10 +0100 Subject: [PATCH 13/22] CI fixes --- test/CoreTest.cpp | 18 +++++++++++++++++- test/SerialIOTest.cpp | 21 ++++++++++++++++++--- test/python/unittest/API/APITest.py | 11 +++++++---- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/test/CoreTest.cpp b/test/CoreTest.cpp index 7c3a5c4ed9..e63e88594f 100644 --- a/test/CoreTest.cpp +++ b/test/CoreTest.cpp @@ -25,6 +25,21 @@ #include #include +// On Windows, REQUIRE() might not be able to print more complex data structures +// upon failure: +// CoreTest.obj : error LNK2001: unresolved external symbol +// "class std::string const Catch::Detail::unprintableString" (...) +#ifdef _WIN32 +#define OPENPMD_REQUIRE_GUARD_WINDOWS(...) \ + do \ + { \ + bool guarded_require_boolean = __VA_ARGS__; \ + REQUIRE(guarded_require_boolean); \ + } while (0); +#else +#define OPENPMD_REQUIRE_GUARD_WINDOWS(...) REQUIRE(__VA_ARGS__) +#endif + using namespace openPMD; Dataset globalDataset(Datatype::CHAR, {1}); @@ -1322,7 +1337,8 @@ TEST_CASE("custom_geometries", "[core]") {{UnitDimension::M, 1}, {UnitDimension::L, 1}, {UnitDimension::T, 2}}}; - REQUIRE(unit_representations::asMaps(E.gridUnitDimension()) == compare); + OPENPMD_REQUIRE_GUARD_WINDOWS( + unit_representations::asMaps(E.gridUnitDimension()) == compare); REQUIRE( E.getAttribute("geometry").get() == "other:customGeometry"); diff --git a/test/SerialIOTest.cpp b/test/SerialIOTest.cpp index 12def16347..7374da7e30 100644 --- a/test/SerialIOTest.cpp +++ b/test/SerialIOTest.cpp @@ -48,6 +48,21 @@ #undef max #endif +// On Windows, REQUIRE() might not be able to print more complex data structures +// upon failure: +// CoreTest.obj : error LNK2001: unresolved external symbol +// "class std::string const Catch::Detail::unprintableString" (...) +#ifdef _WIN32 +#define OPENPMD_REQUIRE_GUARD_WINDOWS(...) \ + do \ + { \ + bool guarded_require_boolean = __VA_ARGS__; \ + REQUIRE(guarded_require_boolean); \ + } while (0); +#else +#define OPENPMD_REQUIRE_GUARD_WINDOWS(...) REQUIRE(__VA_ARGS__) +#endif + using namespace openPMD; struct BackendSelection @@ -953,10 +968,10 @@ inline void constant_scalar(std::string const &file_ending) E_mesh.setAxisLabels(axisLabels); E_mesh.setUnitDimension(unitDimensions); E_mesh.setTimeOffset(timeOffset); - REQUIRE( + OPENPMD_REQUIRE_GUARD_WINDOWS( E_mesh.gridUnitSIPerDimension() == std::vector{1., 1., 1.}); E_mesh.setGridUnitSI(std::vector(3, gridUnitSI)); - REQUIRE( + OPENPMD_REQUIRE_GUARD_WINDOWS( E_mesh.gridUnitSIPerDimension() == std::vector{gridUnitSI, gridUnitSI, gridUnitSI}); @@ -1119,7 +1134,7 @@ inline void constant_scalar(std::string const &file_ending) Extent{3, 2, 1}); Mesh &E_mesh = s.iterations[1].meshes["E"]; - REQUIRE( + OPENPMD_REQUIRE_GUARD_WINDOWS( E_mesh.gridUnitDimension() == std::vector{ std::array{1., 0., 0., 0., 0, .0, 0.}, diff --git a/test/python/unittest/API/APITest.py b/test/python/unittest/API/APITest.py index f828ab9795..a59ffd72a9 100644 --- a/test/python/unittest/API/APITest.py +++ b/test/python/unittest/API/APITest.py @@ -98,10 +98,13 @@ def testFieldData(self): self.assertEqual(len(i.meshes), 2) for m in i.meshes: self.assertTrue(m in ["E", "rho"]) - self.assertEqual(i.meshes[m].unit_dimension, io.Unit_Dimension.as_array( - io.Unit_Dimension.as_map(i.meshes[m].unit_dimension))) - self.assertEqual(io.Unit_Dimension.as_maps(i.meshes[m].grid_unit_dimension), [ - {io.Unit_Dimension.L: 1}, {io.Unit_Dimension.L: 1}]) + self.assertEqual( + i.meshes[m].unit_dimension, + io.Unit_Dimension.as_array( + io.Unit_Dimension.as_map(i.meshes[m].unit_dimension))) + self.assertEqual( + io.Unit_Dimension.as_maps(i.meshes[m].grid_unit_dimension), + [{io.Unit_Dimension.L: 1}, {io.Unit_Dimension.L: 1}]) self.assertEqual(io.Unit_Dimension.from_index(0), io.Unit_Dimension.L) self.assertEqual(io.Unit_Dimension.L.as_index(), 0) for idx in range(7): From 5e8b21d6b88f62cdfd4eee81a302cf9cd9b415dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Wed, 30 Oct 2024 16:24:51 +0100 Subject: [PATCH 14/22] Introduce a member that stores the openPMD version as enum --- include/openPMD/IO/AbstractIOHandler.hpp | 59 +++++++++----------- src/IO/AbstractIOHandler.cpp | 69 ++++++++++++++++++++++++ src/Iteration.cpp | 4 +- src/Series.cpp | 22 +++++--- test/CoreTest.cpp | 4 +- 5 files changed, 113 insertions(+), 45 deletions(-) diff --git a/include/openPMD/IO/AbstractIOHandler.hpp b/include/openPMD/IO/AbstractIOHandler.hpp index 1288a87b21..649252a877 100644 --- a/include/openPMD/IO/AbstractIOHandler.hpp +++ b/include/openPMD/IO/AbstractIOHandler.hpp @@ -25,6 +25,7 @@ #include "openPMD/IO/IOTask.hpp" #include "openPMD/IterationEncoding.hpp" #include "openPMD/config.hpp" +#include "openPMD/version.hpp" #if openPMD_HAVE_MPI #include @@ -75,6 +76,20 @@ enum class FlushLevel CreateOrOpenFiles }; +enum class OpenpmdStandard +{ + v_1_0_0, + v_1_0_1, + v_1_1_0, + v_2_0_0 +}; + +namespace auxiliary +{ + auto parseStandard(std::string const &) -> OpenpmdStandard; + auto formatStandard(OpenpmdStandard) -> char const *; +} // namespace auxiliary + namespace internal { /** @@ -189,38 +204,7 @@ class AbstractIOHandler friend class detail::ADIOS2File; private: - IterationEncoding m_encoding = IterationEncoding::groupBased; - - void setIterationEncoding(IterationEncoding encoding) - { - /* - * In file-based iteration encoding, the APPEND mode is handled entirely - * by the frontend, the backend should just treat it as CREATE mode. - * Similar for READ_LINEAR which should be treated as READ_RANDOM_ACCESS - * in the backend. - */ - if (encoding == IterationEncoding::fileBased) - { - switch (m_backendAccess) - { - - case Access::READ_LINEAR: - // do we really want to have those as const members..? - *const_cast(&m_backendAccess) = - Access::READ_RANDOM_ACCESS; - break; - case Access::APPEND: - *const_cast(&m_backendAccess) = Access::CREATE; - break; - case Access::READ_RANDOM_ACCESS: - case Access::READ_WRITE: - case Access::CREATE: - break; - } - } - - m_encoding = encoding; - } + void setIterationEncoding(IterationEncoding encoding); public: #if openPMD_HAVE_MPI @@ -284,8 +268,14 @@ class AbstractIOHandler */ Access m_backendAccess; Access m_frontendAccess; - internal::SeriesStatus m_seriesStatus = internal::SeriesStatus::Default; std::queue m_work; + + /************************************************************************** + * Since the AbstractIOHandler is linked to every object of the frontend, * + * it stores a number of members that are needed by methods traversing * + * the object hierarchy. Those members are found below. * + **************************************************************************/ + /** * This is to avoid that the destructor tries flushing again if an error * happened. Otherwise, this would lead to confusing error messages. @@ -294,6 +284,9 @@ class AbstractIOHandler * The destructor will only attempt flushing again if this is true. */ bool m_lastFlushSuccessful = false; + internal::SeriesStatus m_seriesStatus = internal::SeriesStatus::Default; + IterationEncoding m_encoding = IterationEncoding::groupBased; + OpenpmdStandard m_standard = auxiliary::parseStandard(getStandardDefault()); }; // AbstractIOHandler } // namespace openPMD diff --git a/src/IO/AbstractIOHandler.cpp b/src/IO/AbstractIOHandler.cpp index 440b663286..3d83978d1d 100644 --- a/src/IO/AbstractIOHandler.cpp +++ b/src/IO/AbstractIOHandler.cpp @@ -21,10 +21,79 @@ #include "openPMD/IO/AbstractIOHandler.hpp" +#include "openPMD/Error.hpp" #include "openPMD/IO/FlushParametersInternal.hpp" +#include + +namespace openPMD::auxiliary +{ +using pair_t = std::pair; +constexpr pair_t STANDARD_VERSIONS[] = { + pair_t{OpenpmdStandard::v_1_0_0, "1.0.0"}, + pair_t{OpenpmdStandard::v_1_0_1, "1.0.1"}, + pair_t{OpenpmdStandard::v_1_1_0, "1.1.0"}, + pair_t{OpenpmdStandard::v_2_0_0, "2.0.0"}}; + +auto parseStandard(const std::string &str) -> OpenpmdStandard +{ + for (auto const &[res, compare] : STANDARD_VERSIONS) + { + if (str == compare) + { + return res; + } + } + throw error::IllegalInOpenPMDStandard( + "Standard version is not supported: '" + str + "'."); +} + +auto formatStandard(OpenpmdStandard std) -> char const * +{ + for (auto const &[compare, res] : STANDARD_VERSIONS) + { + if (std == compare) + { + return res; + } + } + throw error::Internal( + "[auxiliary::formatStandard] Match should be exhaustive."); +} +} // namespace openPMD::auxiliary namespace openPMD { +void AbstractIOHandler::setIterationEncoding(IterationEncoding encoding) +{ + /* + * In file-based iteration encoding, the APPEND mode is handled entirely + * by the frontend, the backend should just treat it as CREATE mode. + * Similar for READ_LINEAR which should be treated as READ_RANDOM_ACCESS + * in the backend. + */ + if (encoding == IterationEncoding::fileBased) + { + switch (m_backendAccess) + { + + case Access::READ_LINEAR: + // do we really want to have those as const members..? + *const_cast(&m_backendAccess) = + Access::READ_RANDOM_ACCESS; + break; + case Access::APPEND: + *const_cast(&m_backendAccess) = Access::CREATE; + break; + case Access::READ_RANDOM_ACCESS: + case Access::READ_WRITE: + case Access::CREATE: + break; + } + } + + m_encoding = encoding; +} + std::future AbstractIOHandler::flush(internal::FlushParams const ¶ms) { internal::ParsedFlushParams parsedParams{params}; diff --git a/src/Iteration.cpp b/src/Iteration.cpp index 366fea0de1..28839fea3b 100644 --- a/src/Iteration.cpp +++ b/src/Iteration.cpp @@ -493,10 +493,10 @@ void Iteration::read_impl(std::string const &groupPath) Series s = retrieveSeries(); Parameter pList; - std::string version = s.openPMD(); + auto version = IOHandler()->m_standard; bool hasMeshes = false; bool hasParticles = false; - if (version == "1.0.0" || version == "1.0.1") + if (version <= OpenpmdStandard::v_1_0_1) { IOHandler()->enqueue(IOTask(this, pList)); IOHandler()->flush(internal::defaultFlushParams); diff --git a/src/Series.cpp b/src/Series.cpp index a3749c5e73..2d10575c17 100644 --- a/src/Series.cpp +++ b/src/Series.cpp @@ -139,12 +139,13 @@ std::string Series::openPMD() const Series &Series::setOpenPMD(std::string const &o) { - if (o >= "2.0") + setAttribute("openPMD", o); + auto standard = auxiliary::parseStandard(o); + IOHandler()->m_standard = standard; + if (standard >= OpenpmdStandard::v_2_0_0) { - std::cerr << "[Warning] openPMD 2.0 is still under development." - << std::endl; + std::cerr << "[Warning] openPMD 2.0 is still under development.\n"; } - setAttribute("openPMD", o); return *this; } @@ -166,11 +167,15 @@ std::string Series::basePath() const Series &Series::setBasePath(std::string const &bp) { - std::string version = openPMD(); - if (version == "1.0.0" || version == "1.0.1" || version == "1.1.0" || - version == "2.0.0") + switch (IOHandler()->m_standard) + { + case OpenpmdStandard::v_1_0_0: + case OpenpmdStandard::v_1_0_1: + case OpenpmdStandard::v_1_1_0: + case OpenpmdStandard::v_2_0_0: throw std::runtime_error( "Custom basePath not allowed in openPMD <=2.0"); + } setAttribute("basePath", bp); return *this; @@ -630,7 +635,8 @@ Series &Series::setIterationFormat(std::string const &i) setBasePath(i); } else if ( - basePath() != i && (openPMD() == "1.0.1" || openPMD() == "1.0.0")) + basePath() != i && + IOHandler()->m_standard <= OpenpmdStandard::v_1_0_1) throw std::invalid_argument( "iterationFormat must not differ from basePath " + basePath() + " for group- or variableBased data"); diff --git a/test/CoreTest.cpp b/test/CoreTest.cpp index e63e88594f..2c4a0b1680 100644 --- a/test/CoreTest.cpp +++ b/test/CoreTest.cpp @@ -828,10 +828,10 @@ TEST_CASE("wrapper_test", "[core]") REQUIRE(copy.openPMDextension() == 42); REQUIRE(copy.iterationEncoding() == IterationEncoding::fileBased); REQUIRE(copy.name() == "new_openpmd_output_%T"); - copy.setOpenPMD("1.2.0"); + copy.setOpenPMD("1.1.0"); copy.setIterationEncoding(IterationEncoding::groupBased); copy.setName("other_name"); - REQUIRE(o.openPMD() == "1.2.0"); + REQUIRE(o.openPMD() == "1.1.0"); REQUIRE(o.iterationEncoding() == IterationEncoding::groupBased); REQUIRE(o.name() == "other_name"); From c93208075abd19c58f16733803ed64172f1e495c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Wed, 30 Oct 2024 16:59:58 +0100 Subject: [PATCH 15/22] Use enum-type standard check for meshes --- src/Mesh.cpp | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index e2e239299e..57659d9009 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -184,19 +184,15 @@ double Mesh::gridUnitSI() const Mesh &Mesh::setGridUnitSI(double gusi) { setAttribute("gridUnitSI", gusi); - if (auto series_opt = retrieveSeries_optional(); series_opt.has_value()) + if (auto standard = IOHandler()->m_standard; + standard >= OpenpmdStandard::v_2_0_0) { - if (auto version = series_opt->openPMD(); version >= "2.") - { - std::cerr << "[Mesh::setGridUnitSI] Warning: Setting a scalar " - "`gridUnitSI` in a file with openPMD version '" + - version + - "'. Consider specifying a vector instead in order to " - "specify " - "the gridUnitSI per axis (ref.: " - "https://github.com/openPMD/openPMD-standard/pull/193)." - << std::endl; - } + std::cerr << "[Mesh::setGridUnitSI] Warning: Setting a scalar " + "`gridUnitSI` in a file with openPMD version '" + + std::string(auxiliary::formatStandard(standard)) + + "'. Consider specifying a vector instead in order to " + "specify the gridUnitSI per axis (ref.: " + "https://github.com/openPMD/openPMD-standard/pull/193).\n"; } return *this; } @@ -234,7 +230,7 @@ std::vector Mesh::gridUnitSIPerDimension() const { if (auto series_opt = retrieveSeries_optional(); series_opt.has_value()) { - if (auto version = series_opt->openPMD(); version < "2.") + if (IOHandler()->m_standard < OpenpmdStandard::v_2_0_0) { // If the openPMD version is lower than 2.0, the gridUnitSI is a // scalar interpreted for all axes. Copy it d times. @@ -258,12 +254,13 @@ Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) setAttribute("gridUnitSI", std::move(gridUnitSI)); if (auto series_opt = retrieveSeries_optional(); series_opt.has_value()) { - if (auto version = series_opt->openPMD(); version < "2.") + if (auto standard = IOHandler()->m_standard; + standard < OpenpmdStandard::v_2_0_0) { throw error::IllegalInOpenPMDStandard( "[Mesh::setGridUnitSI] Setting `gridUnitSI` as a vector in a " "file with openPMD version '" + - version + + std::string(auxiliary::formatStandard(standard)) + "', but per-axis specification is only supported as of " "openPMD 2.0. Either upgrade the file to openPMD >= 2.0 " "or specify a scalar that applies to all axes."); @@ -410,7 +407,7 @@ void Mesh::flush_impl( } if (!containsAttribute("gridUnitSI")) { - if (auto series = retrieveSeries(); series.openPMD() < "2.") + if (IOHandler()->m_standard < OpenpmdStandard::v_2_0_0) { setGridUnitSI(1); } @@ -542,8 +539,7 @@ void Mesh::read() aRead.name = "gridUnitSI"; IOHandler()->enqueue(IOTask(this, aRead)); IOHandler()->flush(internal::defaultFlushParams); - auto series = retrieveSeries(); - if (series.openPMD() >= "2.") + if (IOHandler()->m_standard >= OpenpmdStandard::v_2_0_0) { if (auto val = Attribute(*aRead.resource).getOptional>(); From 21d1eefbee000d370bcf13f431861a98e23d62f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 4 Nov 2024 10:44:54 +0100 Subject: [PATCH 16/22] Move auxiliary namespace under unit_representation --- include/openPMD/UnitDimension.hpp | 12 ++--- src/Mesh.cpp | 6 ++- src/UnitDimension.cpp | 89 +++++++++++++++---------------- 3 files changed, 52 insertions(+), 55 deletions(-) diff --git a/include/openPMD/UnitDimension.hpp b/include/openPMD/UnitDimension.hpp index b6b82d4ea3..e1773fc7a6 100644 --- a/include/openPMD/UnitDimension.hpp +++ b/include/openPMD/UnitDimension.hpp @@ -58,11 +58,11 @@ namespace unit_representations auto asArrays(AsMaps const &) -> AsArrays; auto asMaps(AsArrays const &) -> AsMaps; -} // namespace unit_representations -namespace auxiliary -{ - void fromMapOfUnitDimension( - double *cursor, std::map const &udim); -} // namespace auxiliary + namespace auxiliary + { + void fromMapOfUnitDimension( + double *cursor, std::map const &udim); + } // namespace auxiliary +} // namespace unit_representations } // namespace openPMD diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 57659d9009..f4875b4602 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -20,6 +20,7 @@ */ #include "openPMD/Mesh.hpp" #include "openPMD/Error.hpp" +#include "openPMD/IO/AbstractIOHandler.hpp" #include "openPMD/Series.hpp" #include "openPMD/ThrowError.hpp" #include "openPMD/UnitDimension.hpp" @@ -274,7 +275,8 @@ Mesh &Mesh::setUnitDimension(std::map const &udim) if (!udim.empty()) { std::array tmpUnitDimension = this->unitDimension(); - auxiliary::fromMapOfUnitDimension(tmpUnitDimension.data(), udim); + unit_representations::auxiliary::fromMapOfUnitDimension( + tmpUnitDimension.data(), udim); setAttribute("unitDimension", tmpUnitDimension); } return *this; @@ -298,7 +300,7 @@ Mesh &Mesh::setGridUnitDimension( auto cursor = rawGridUnitDimension.begin(); for (auto const &udim : udims) { - auxiliary::fromMapOfUnitDimension(&*cursor, udim); + unit_representations::auxiliary::fromMapOfUnitDimension(&*cursor, udim); cursor += 7; } setAttribute("gridUnitDimension", rawGridUnitDimension); diff --git a/src/UnitDimension.cpp b/src/UnitDimension.cpp index c9aa449347..103fdea6c5 100644 --- a/src/UnitDimension.cpp +++ b/src/UnitDimension.cpp @@ -2,62 +2,57 @@ #include #include -namespace openPMD +namespace openPMD::unit_representations { -namespace auxiliary +auto asArray(AsMap const &udim) -> AsArray { - void fromMapOfUnitDimension( - double *cursor, std::map const &udim) + AsArray res{}; + auxiliary::fromMapOfUnitDimension(res.data(), udim); + return res; +} +auto asMap(AsArray const &array) -> AsMap +{ + AsMap udim; + for (size_t i = 0; i < array.size(); ++i) { - for (auto [unit, exponent] : udim) + if (array[i] != 0) { - cursor[static_cast(unit)] = exponent; + udim[static_cast(i)] = array[i]; } } -} // namespace auxiliary + return udim; +} -namespace unit_representations +auto asArrays(AsMaps const &vec) -> AsArrays { - auto asArray(AsMap const &udim) -> AsArray - { - AsArray res{}; - auxiliary::fromMapOfUnitDimension(res.data(), udim); - return res; - } - auto asMap(AsArray const &array) -> AsMap + AsArrays res; + res.reserve(vec.size()); + std::transform( + vec.begin(), vec.end(), std::back_inserter(res), [](auto const &map) { + return asArray(map); + }); + return res; +} +auto asMaps(AsArrays const &vec) -> AsMaps +{ + AsMaps res; + res.reserve(vec.size()); + std::transform( + vec.begin(), vec.end(), std::back_inserter(res), [](auto const &array) { + return asMap(array); + }); + return res; +} + +namespace auxiliary +{ + void fromMapOfUnitDimension( + double *cursor, std::map const &udim) { - AsMap udim; - for (size_t i = 0; i < array.size(); ++i) + for (auto [unit, exponent] : udim) { - if (array[i] != 0) - { - udim[static_cast(i)] = array[i]; - } + cursor[static_cast(unit)] = exponent; } - return udim; } - - auto asArrays(AsMaps const &vec) -> AsArrays - { - AsArrays res; - res.reserve(vec.size()); - std::transform( - vec.begin(), - vec.end(), - std::back_inserter(res), - [](auto const &map) { return asArray(map); }); - return res; - } - auto asMaps(AsArrays const &vec) -> AsMaps - { - AsMaps res; - res.reserve(vec.size()); - std::transform( - vec.begin(), - vec.end(), - std::back_inserter(res), - [](auto const &array) { return asMap(array); }); - return res; - } -} // namespace unit_representations -} // namespace openPMD +} // namespace auxiliary +} // namespace openPMD::unit_representations From 63530634c49bacad02afd1e9aa50ca71dc5926b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 4 Nov 2024 10:54:13 +0100 Subject: [PATCH 17/22] Remove retrieveSeries_optional --- .../openPMD/auxiliary/DerefDynamicCast.hpp | 33 +---------------- include/openPMD/backend/Attributable.hpp | 3 -- src/Mesh.cpp | 36 ++++++++----------- src/backend/Attributable.cpp | 30 ++-------------- 4 files changed, 18 insertions(+), 84 deletions(-) diff --git a/include/openPMD/auxiliary/DerefDynamicCast.hpp b/include/openPMD/auxiliary/DerefDynamicCast.hpp index ecbf2033e1..1620334ea0 100644 --- a/include/openPMD/auxiliary/DerefDynamicCast.hpp +++ b/include/openPMD/auxiliary/DerefDynamicCast.hpp @@ -20,9 +20,7 @@ */ #pragma once -#include -#include -#include +#include namespace openPMD { @@ -49,34 +47,5 @@ namespace auxiliary return *tmp_ptr; } - /** Returns a value reference stored in a dynamically casted pointer - * - * Safe version of *dynamic_cast< New_Type* >( some_ptr ); This function - * will throw as dynamic_cast and will furthermore throw if the result - * of the dynamic_cast is a nullptr. - * - * @tparam New_Type new type to cast to - * @tparam Old_Type old type to cast from - * @param[in] ptr and input pointer type - * @return value reference of a dereferenced, dynamically casted ptr to - * New_Type* - */ - template - inline std::optional - dynamic_cast_optional(Old_Type *ptr) noexcept - { - try - { - auto const tmp_ptr = dynamic_cast(ptr); - if (tmp_ptr == nullptr) - return std::nullopt; - return {tmp_ptr}; - } - catch (std::bad_cast const &) - { - return std::nullopt; - } - } - } // namespace auxiliary } // namespace openPMD diff --git a/include/openPMD/backend/Attributable.hpp b/include/openPMD/backend/Attributable.hpp index 85d3712277..850d3bf35c 100644 --- a/include/openPMD/backend/Attributable.hpp +++ b/include/openPMD/backend/Attributable.hpp @@ -30,7 +30,6 @@ #include #include #include -#include #include #include #include @@ -333,8 +332,6 @@ class Attributable OPENPMD_protected // clang-format on - std::optional retrieveSeries_optional() const; - Series retrieveSeries() const; /** Returns the corresponding Iteration diff --git a/src/Mesh.cpp b/src/Mesh.cpp index f4875b4602..9c8fb0a078 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -229,16 +229,13 @@ std::vector Mesh::gridUnitSIPerDimension() const { if (containsAttribute("gridUnitSI")) { - if (auto series_opt = retrieveSeries_optional(); series_opt.has_value()) + if (IOHandler()->m_standard < OpenpmdStandard::v_2_0_0) { - if (IOHandler()->m_standard < OpenpmdStandard::v_2_0_0) - { - // If the openPMD version is lower than 2.0, the gridUnitSI is a - // scalar interpreted for all axes. Copy it d times. - return std::vector( - retrieveMeshDimensionality(*this), - getAttribute("gridUnitSI").get()); - } + // If the openPMD version is lower than 2.0, the gridUnitSI is a + // scalar interpreted for all axes. Copy it d times. + return std::vector( + retrieveMeshDimensionality(*this), + getAttribute("gridUnitSI").get()); } return getAttribute("gridUnitSI").get>(); } @@ -253,19 +250,16 @@ std::vector Mesh::gridUnitSIPerDimension() const Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) { setAttribute("gridUnitSI", std::move(gridUnitSI)); - if (auto series_opt = retrieveSeries_optional(); series_opt.has_value()) + if (auto standard = IOHandler()->m_standard; + standard < OpenpmdStandard::v_2_0_0) { - if (auto standard = IOHandler()->m_standard; - standard < OpenpmdStandard::v_2_0_0) - { - throw error::IllegalInOpenPMDStandard( - "[Mesh::setGridUnitSI] Setting `gridUnitSI` as a vector in a " - "file with openPMD version '" + - std::string(auxiliary::formatStandard(standard)) + - "', but per-axis specification is only supported as of " - "openPMD 2.0. Either upgrade the file to openPMD >= 2.0 " - "or specify a scalar that applies to all axes."); - } + throw error::IllegalInOpenPMDStandard( + "[Mesh::setGridUnitSI] Setting `gridUnitSI` as a vector in a " + "file with openPMD version '" + + std::string(auxiliary::formatStandard(standard)) + + "', but per-axis specification is only supported as of " + "openPMD 2.0. Either upgrade the file to openPMD >= 2.0 " + "or specify a scalar that applies to all axes."); } return *this; } diff --git a/src/backend/Attributable.cpp b/src/backend/Attributable.cpp index dd7f5a621a..9ffbe25ee3 100644 --- a/src/backend/Attributable.cpp +++ b/src/backend/Attributable.cpp @@ -30,7 +30,6 @@ #include #include #include -#include #include #include #include @@ -131,39 +130,14 @@ void Attributable::iterationFlush(std::string backendConfig) std::move(backendConfig)); } -std::optional Attributable::retrieveSeries_optional() const +Series Attributable::retrieveSeries() const { Writable const *findSeries = &writable(); while (findSeries->parent) { findSeries = findSeries->parent; } - auto maybeSeriesData = - auxiliary::dynamic_cast_optional( - findSeries->attributable); - if (!maybeSeriesData.has_value()) - { - return std::nullopt; - } - auto seriesData = *maybeSeriesData; - return seriesData->asInternalCopyOf(); -} - -Series Attributable::retrieveSeries() const -{ - if (auto maybeSeries = retrieveSeries_optional(); maybeSeries.has_value()) - { - return *maybeSeries; - } - else - { - throw std::runtime_error( - "[Attributable::retrieveSeries] Error when trying to retrieve the " - "Series object. Note: An instance of the Series object must still " - "exist when flushing. A common cause for this error is using a " - "flush call on a handle (e.g. `Iteration::seriesFlush()`) when the " - "original Series object has already gone out of scope."); - } + return findSeries->attributable->asInternalCopyOf(); } auto Attributable::containingIteration() const -> std::pair< From 6ae9e1cd6fbb1843ebc1b398f5c10e80fc25160c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 4 Nov 2024 15:27:28 +0100 Subject: [PATCH 18/22] Cleaner Python API, Python tests --- include/openPMD/Mesh.hpp | 10 ++- include/openPMD/Record.hpp | 4 +- include/openPMD/backend/Attributable.hpp | 2 + src/Mesh.cpp | 7 +- src/Record.cpp | 3 +- src/backend/Attributable.cpp | 6 ++ src/binding/python/Mesh.cpp | 30 ++++++-- test/python/unittest/API/APITest.py | 89 ++++++++++++++++++++++++ 8 files changed, 131 insertions(+), 20 deletions(-) diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index 119111ea53..70d8591ddd 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -25,7 +25,6 @@ #include "openPMD/backend/BaseRecord.hpp" #include "openPMD/backend/MeshRecordComponent.hpp" -#include #include #include #include @@ -196,7 +195,7 @@ class Mesh : public BaseRecord */ Mesh &setGridUnitSI(double gridUnitSI); - /** Alias for `setGridUnitSI(std::vector)`. + /** Alias for `setGridUnitSIPerDimension(std::vector)`. * * Set the unit-conversion factor per dimension to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from @@ -245,8 +244,7 @@ class Mesh : public BaseRecord * that represent the power of the particular base. * @return Reference to modified mesh. */ - Mesh & - setUnitDimension(std::map const &unitDimension); + Mesh &setUnitDimension(unit_representations::AsMap const &unitDimension); /** * @brief Set the unitDimension for each axis of the current grid. @@ -260,8 +258,8 @@ class Mesh : public BaseRecord * * @return Reference to modified mesh. */ - Mesh &setGridUnitDimension( - std::vector> const &gridUnitDimension); + Mesh & + setGridUnitDimension(unit_representations::AsMaps const &gridUnitDimension); /** * @brief Return the physical dimensions of the mesh axes. diff --git a/include/openPMD/Record.hpp b/include/openPMD/Record.hpp index c875389db5..957536bf2c 100644 --- a/include/openPMD/Record.hpp +++ b/include/openPMD/Record.hpp @@ -21,9 +21,9 @@ #pragma once #include "openPMD/RecordComponent.hpp" +#include "openPMD/UnitDimension.hpp" #include "openPMD/backend/BaseRecord.hpp" -#include #include #include @@ -40,7 +40,7 @@ class Record : public BaseRecord Record &operator=(Record const &) = default; ~Record() override = default; - Record &setUnitDimension(std::map const &); + Record &setUnitDimension(unit_representations::AsMap const &); template T timeOffset() const; diff --git a/include/openPMD/backend/Attributable.hpp b/include/openPMD/backend/Attributable.hpp index 850d3bf35c..6ff5b85ed1 100644 --- a/include/openPMD/backend/Attributable.hpp +++ b/include/openPMD/backend/Attributable.hpp @@ -328,6 +328,8 @@ class Attributable */ void touch(); + [[nodiscard]] OpenpmdStandard openPMDStandard() const; + // clang-format off OPENPMD_protected // clang-format on diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 9c8fb0a078..5f7c5c34ef 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -249,7 +249,6 @@ std::vector Mesh::gridUnitSIPerDimension() const Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) { - setAttribute("gridUnitSI", std::move(gridUnitSI)); if (auto standard = IOHandler()->m_standard; standard < OpenpmdStandard::v_2_0_0) { @@ -261,10 +260,11 @@ Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) "openPMD 2.0. Either upgrade the file to openPMD >= 2.0 " "or specify a scalar that applies to all axes."); } + setAttribute("gridUnitSI", std::move(gridUnitSI)); return *this; } -Mesh &Mesh::setUnitDimension(std::map const &udim) +Mesh &Mesh::setUnitDimension(unit_representations::AsMap const &udim) { if (!udim.empty()) { @@ -276,8 +276,7 @@ Mesh &Mesh::setUnitDimension(std::map const &udim) return *this; } -Mesh &Mesh::setGridUnitDimension( - std::vector> const &udims) +Mesh &Mesh::setGridUnitDimension(unit_representations::AsMaps const &udims) { auto rawGridUnitDimension = [this]() { try diff --git a/src/Record.cpp b/src/Record.cpp index 3bcac4d7e1..ad3771bdfe 100644 --- a/src/Record.cpp +++ b/src/Record.cpp @@ -20,6 +20,7 @@ */ #include "openPMD/Record.hpp" #include "openPMD/RecordComponent.hpp" +#include "openPMD/UnitDimension.hpp" #include "openPMD/backend/BaseRecord.hpp" #include @@ -31,7 +32,7 @@ Record::Record() setTimeOffset(0.f); } -Record &Record::setUnitDimension(std::map const &udim) +Record &Record::setUnitDimension(unit_representations::AsMap const &udim) { if (!udim.empty()) { diff --git a/src/backend/Attributable.cpp b/src/backend/Attributable.cpp index 9ffbe25ee3..e7b48efddf 100644 --- a/src/backend/Attributable.cpp +++ b/src/backend/Attributable.cpp @@ -19,6 +19,7 @@ * If not, see . */ #include "openPMD/backend/Attributable.hpp" +#include "openPMD/IO/AbstractIOHandler.hpp" #include "openPMD/Iteration.hpp" #include "openPMD/ParticleSpecies.hpp" #include "openPMD/RecordComponent.hpp" @@ -250,6 +251,11 @@ void Attributable::touch() setDirtyRecursive(true); } +OpenpmdStandard Attributable::openPMDStandard() const +{ + return IOHandler()->m_standard; +} + template void Attributable::seriesFlush_impl(internal::FlushParams const &flushParams) { diff --git a/src/binding/python/Mesh.cpp b/src/binding/python/Mesh.cpp index 9ec65e7f53..604a45d892 100644 --- a/src/binding/python/Mesh.cpp +++ b/src/binding/python/Mesh.cpp @@ -19,6 +19,8 @@ * If not, see . */ #include "openPMD/Mesh.hpp" +#include "openPMD/Error.hpp" +#include "openPMD/IO/AbstractIOHandler.hpp" #include "openPMD/backend/Attributable.hpp" #include "openPMD/backend/BaseRecord.hpp" #include "openPMD/backend/MeshRecordComponent.hpp" @@ -29,6 +31,7 @@ #include "openPMD/binding/python/UnitDimension.hpp" #include +#include #include void init_Mesh(py::module &m) @@ -36,7 +39,7 @@ void init_Mesh(py::module &m) auto py_m_cont = declare_container(m, "Mesh_Container"); - py::class_ > cl(m, "Mesh"); + py::class_> cl(m, "Mesh"); py::enum_(m, "Geometry") // TODO: m -> cl .value("cartesian", Mesh::Geometry::cartesian) @@ -102,12 +105,25 @@ void init_Mesh(py::module &m) &Mesh::setGridGlobalOffset) .def_property( "grid_unit_SI", - &Mesh::gridUnitSI, - py::overload_cast(&Mesh::setGridUnitSI)) - .def_property( - "grid_unit_SI_per_dimension", - &Mesh::gridUnitSIPerDimension, - &Mesh::setGridUnitSIPerDimension) + [](Mesh &self) { + using return_t = std::variant>; + if (self.openPMDStandard() < OpenpmdStandard::v_2_0_0) + { + return return_t(self.gridUnitSI()); + } + else + { + return return_t(self.gridUnitSIPerDimension()); + } + }, + [](Mesh &self, std::variant> arg) { + return std::visit( + [&](auto &&arg_resolved) { + return self.setGridUnitSI( + static_cast(arg_resolved)); + }, + arg); + }) .def_property( "time_offset", &Mesh::timeOffset, diff --git a/test/python/unittest/API/APITest.py b/test/python/unittest/API/APITest.py index a59ffd72a9..d866091571 100644 --- a/test/python/unittest/API/APITest.py +++ b/test/python/unittest/API/APITest.py @@ -412,6 +412,95 @@ def testAttributes(self): for ext in tested_file_extensions: self.attributeRoundTrip(ext) + def testOpenPMD_2_0(self): + write_2_0 = io.Series("../samples/openpmd_2_0.json", io.Access.create) + write_2_0.openPMD = "2.0.0" + meshes = write_2_0.write_iterations()[100].meshes + + E = meshes["E"] + E.reset_dataset(io.Dataset(io.Datatype.DOUBLE, [10, 10, 10])) + E.grid_unit_SI = [1, 2, 3] + E.grid_unit_dimension = [ + {io.Unit_Dimension.L: 1}, + {io.Unit_Dimension.L: 1}, + {io.Unit_Dimension.L: 1, io.Unit_Dimension.T: -1}] + E.make_constant(17) + + B = meshes["B"] + B.reset_dataset(io.Dataset(io.Datatype.DOUBLE, [10, 10, 10])) + # This is deprecated for openPMD 2.0, a warning will be printed. + B.grid_unit_SI = 3 + B.grid_unit_dimension = [{io.Unit_Dimension.L: 1} for _ in range(3)] + B.make_constant(18) + + write_2_0.close() + + read_2_0 = io.Series( + "../samples/openpmd_2_0.json", io.Access.read_only) + meshes = read_2_0.iterations[100].meshes + + E = meshes["E"] + self.assertEqual(E.grid_unit_SI, [1, 2, 3]) + self.assertEqual(E.grid_unit_dimension, io.Unit_Dimension.as_arrays([ + {io.Unit_Dimension.L: 1}, + {io.Unit_Dimension.L: 1}, + {io.Unit_Dimension.L: 1, io.Unit_Dimension.T: -1}])) + + B = meshes["B"] + # Will return a list due to openPMD standard being set to 2.0.0 + self.assertEqual(B.grid_unit_SI, [3]) + self.assertEqual(io.Unit_Dimension.as_maps(B.grid_unit_dimension), [ + {io.Unit_Dimension.L: 1} for _ in range(3)]) + read_2_0.close() + + write_1_1 = io.Series("../samples/openpmd_1_1.json", io.Access.create) + write_1_1.openPMD = "1.1.0" + meshes = write_1_1.write_iterations()[100].meshes + + E = meshes["E"] + E.reset_dataset(io.Dataset(io.Datatype.DOUBLE, [10, 10, 10])) + + def unsupported_in_1_1(): + E.grid_unit_SI = [1, 2, 3] + self.assertRaises( + io.ErrorIllegalInOpenPMDStandard, unsupported_in_1_1) + E.grid_unit_dimension = [ + {io.Unit_Dimension.L: 1}, + {io.Unit_Dimension.L: 1}, + {io.Unit_Dimension.L: 1, io.Unit_Dimension.T: -1}] + E.make_constant(17) + + B = meshes["B"] + B.reset_dataset(io.Dataset(io.Datatype.DOUBLE, [10, 10, 10])) + # This is deprecated for openPMD 2.0, a warning will be printed. + B.grid_unit_SI = 3 + B.grid_unit_dimension = [{io.Unit_Dimension.L: 1} for _ in range(3)] + B.make_constant(18) + + write_1_1.close() + + read_1_1 = io.Series( + "../samples/openpmd_1_1.json", io.Access.read_only) + meshes = read_1_1.iterations[100].meshes + + E = meshes["E"] + # Will return a default value due to the failed attempt at setting + # a list at write time + self.assertEqual(E.grid_unit_SI, 1) + self.assertEqual(E.grid_unit_dimension, io.Unit_Dimension.as_arrays([ + {io.Unit_Dimension.L: 1}, + {io.Unit_Dimension.L: 1}, + {io.Unit_Dimension.L: 1, io.Unit_Dimension.T: -1}])) + + B = meshes["B"] + # Will return a scalar due to openPMD standard being set to 2.0.0 + self.assertEqual(B.grid_unit_SI, 3) + self.assertEqual(io.Unit_Dimension.as_maps(B.grid_unit_dimension), [ + {io.Unit_Dimension.L: 1} for _ in range(3)]) + read_1_1.close() + + + def makeConstantRoundTrip(self, file_ending): # write series = io.Series( From 44decb6620e3fb642935bf1909362a9235810e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 4 Nov 2024 16:12:26 +0100 Subject: [PATCH 19/22] Python documentation, C++ documentation fixes --- include/openPMD/Mesh.hpp | 8 ++--- .../openPMD/binding/python/UnitDimension.hpp | 26 +++++++++++++-- src/binding/python/Mesh.cpp | 33 +++++++++++++++++-- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index 70d8591ddd..e6d70a5e7e 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -197,7 +197,7 @@ class Mesh : public BaseRecord /** Alias for `setGridUnitSIPerDimension(std::vector)`. * - * Set the unit-conversion factor per dimension to multiply each value in + * Set the unit-conversion factors per axis to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from * simulation units to SI units. * @@ -214,14 +214,14 @@ class Mesh : public BaseRecord Mesh &setGridUnitSI(std::vector gridUnitSI); /** - * @return A vector of the gridUnitSI per grid dimension as defined - * by the axisLabels. If the gridUnitSI is defined as a scalar + * @return A vector of the gridUnitSI per grid axis in the order of + * the axisLabels. If the gridUnitSI is defined as a scalar * (legacy openPMD), the dimensionality is determined and a vector of * `dimensionality` times the scalar vector is returned. */ std::vector gridUnitSIPerDimension() const; - /* Set the unit-conversion factor per dimension to multiply each value in + /* Set the unit-conversion factors per axis to multiply each value in * Mesh::gridSpacing and Mesh::gridGlobalOffset, in order to convert from * simulation units to SI units. * diff --git a/include/openPMD/binding/python/UnitDimension.hpp b/include/openPMD/binding/python/UnitDimension.hpp index ab4cbde650..b7a402f0f6 100644 --- a/include/openPMD/binding/python/UnitDimension.hpp +++ b/include/openPMD/binding/python/UnitDimension.hpp @@ -24,7 +24,7 @@ namespace openPMD { namespace python { - constexpr auto doc_unit_dimension = R"docstr( + constexpr auto doc_unit_dimension = &R"docstr( Return the physical dimension (quantity) of a record Annotating the physical dimension of a record allows us to read data @@ -40,7 +40,29 @@ See https://en.wikipedia.org/wiki/International_System_of_Quantities#Base_quanti See https://github.com/openPMD/openPMD-standard/blob/1.1.0/STANDARD.md#required-for-each-record Returns the powers of the 7 base measures in the order specified above. -)docstr"; +)docstr"[1]; + + constexpr auto doc_mesh_unit_dimension = &R"docstr( +Return the physical dimension (quantity) of the record axes + +Annotating the physical dimension of the record axes allows us to read data +sets with arbitrary names and understand their purpose simply by +dimensional analysis. The dimensional base quantities in openPMD are +in order: length (L), mass (M), time (T), electric current (I), +thermodynamic temperature (theta), amount of substance (N), +luminous intensity (J) after the international system of quantities +(ISQ). +This attribute may be left out, the axes will then be interpreted as spatial. + +See https://en.wikipedia.org/wiki/Dimensional_analysis +See https://en.wikipedia.org/wiki/International_System_of_Quantities#Base_quantities +See https://github.com/openPMD/openPMD-standard/blob/1.1.0/STANDARD.md#required-for-each-record + +Returns the powers of the 7 base measures in the order specified above, listed +for each axis in the order of the axisLabels. +This attribute has been introduced as part of openPMD 2.0.0 in: +Ref.: https://github.com/openPMD/openPMD-standard/pull/193 +)docstr"[1]; } // namespace python } // namespace openPMD diff --git a/src/binding/python/Mesh.cpp b/src/binding/python/Mesh.cpp index 604a45d892..0346a68dfd 100644 --- a/src/binding/python/Mesh.cpp +++ b/src/binding/python/Mesh.cpp @@ -67,7 +67,8 @@ void init_Mesh(py::module &m) .def_property( "grid_unit_dimension", &Mesh::gridUnitDimension, - &Mesh::setGridUnitDimension) + &Mesh::setGridUnitDimension, + python::doc_mesh_unit_dimension) .def_property( "geometry", @@ -105,6 +106,13 @@ void init_Mesh(py::module &m) &Mesh::setGridGlobalOffset) .def_property( "grid_unit_SI", + /* + * Using pybind11's support for std::variant in order to implement a + * polymorphic type for this property. Will be a scalar double in + * openPMD 1.*, a list of double in openPMD 2.*. + * Unlike in the C++ API, this means that no new API calls + * such as gridUnitSIPerDimension() must be added. + */ [](Mesh &self) { using return_t = std::variant>; if (self.openPMDStandard() < OpenpmdStandard::v_2_0_0) @@ -123,7 +131,28 @@ void init_Mesh(py::module &m) static_cast(arg_resolved)); }, arg); - }) + }, + &R"( +For openPMD versions 1.*: + +Set the unit-conversion factor to multiply each value in +Mesh.grid_spacing and Mesh.grid_global_offset, in order to convert from +simulation units to SI units. +The type is a scalar floating point. + +For openPMD versions 2.*: + +Set the unit-conversion **factors per axis** in the order of the axisLabels +to multiply each value in Mesh.grid_spacing and Mesh.grid_global_offset, +in order to convert from simulation units to SI units. +The type is a list of floating points. + +When writing a scalar value to an openPMD 2.* file, a warning will be printed +(for enabling a more comfortable migration to openPMD 2.*). +When writing a list value to an openPMD 1.* file, an error will be thrown, +since most openPMD 1.*-based readers will not be able to interpret this +properly. +Ref.: https://github.com/openPMD/openPMD-standard/pull/193)"[1]) .def_property( "time_offset", &Mesh::timeOffset, From 054ae0cb64336ad1409905fccb2aae0fc672111c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 4 Nov 2024 16:53:22 +0100 Subject: [PATCH 20/22] Better function argument types --- include/openPMD/Mesh.hpp | 30 ++++++++++++++++++++--- include/openPMD/Record.hpp | 1 + include/openPMD/UnitDimension.hpp | 4 ++-- src/Mesh.cpp | 22 +++++++++++++---- src/Record.cpp | 5 ++++ src/UnitDimension.cpp | 13 +++++----- src/binding/python/Mesh.cpp | 36 ++++++++++++++++++++++------ src/binding/python/Record.cpp | 17 +++++++++++-- src/binding/python/UnitDimension.cpp | 10 ++++++-- 9 files changed, 111 insertions(+), 27 deletions(-) diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp index e6d70a5e7e..77ef8b2886 100644 --- a/include/openPMD/Mesh.hpp +++ b/include/openPMD/Mesh.hpp @@ -211,7 +211,7 @@ class Mesh : public BaseRecord * * @return Reference to modified mesh. */ - Mesh &setGridUnitSI(std::vector gridUnitSI); + Mesh &setGridUnitSI(std::vector const &gridUnitSI); /** * @return A vector of the gridUnitSI per grid axis in the order of @@ -235,7 +235,7 @@ class Mesh : public BaseRecord * * @return Reference to modified mesh. */ - Mesh &setGridUnitSIPerDimension(std::vector gridUnitSI); + Mesh &setGridUnitSIPerDimension(std::vector const &gridUnitSI); /** Set the powers of the 7 base measures characterizing the record's unit * in SI. @@ -246,11 +246,20 @@ class Mesh : public BaseRecord */ Mesh &setUnitDimension(unit_representations::AsMap const &unitDimension); + /** Set the powers of the 7 base measures characterizing the record's unit + * in SI. + * + * @param unitDimension array containing seven doubles, each + * representing the power of the particular base in order. + * @return Reference to modified mesh. + */ + Mesh &setUnitDimension(unit_representations::AsArray const &unitDimension); + /** * @brief Set the unitDimension for each axis of the current grid. * * @param gridUnitDimension A vector of the unitDimensions for each - * axis of the grid in the order of the axisLabels. + * axis of the grid in the order of the axisLabels, in dict representation. * Behavior note: This is an updating method, meaning that an SI unit that * has been defined before and is in the next call not explicitly set @@ -261,6 +270,21 @@ class Mesh : public BaseRecord Mesh & setGridUnitDimension(unit_representations::AsMaps const &gridUnitDimension); + /** + * @brief Set the unitDimension for each axis of the current grid. + * + * @param gridUnitDimension A vector of the unitDimensions for each + * axis of the grid in the order of the axisLabels, in array representation. + + * Behavior note: This is an updating method, meaning that an SI unit that + * has been defined before and is in the next call not explicitly set + * in the `std::map` will keep its previous value. + * + * @return Reference to modified mesh. + */ + Mesh &setGridUnitDimension( + unit_representations::AsArrays const &gridUnitDimension); + /** * @brief Return the physical dimensions of the mesh axes. diff --git a/include/openPMD/Record.hpp b/include/openPMD/Record.hpp index 957536bf2c..791c4c15f8 100644 --- a/include/openPMD/Record.hpp +++ b/include/openPMD/Record.hpp @@ -41,6 +41,7 @@ class Record : public BaseRecord ~Record() override = default; Record &setUnitDimension(unit_representations::AsMap const &); + Record &setUnitDimension(unit_representations::AsArray const &); template T timeOffset() const; diff --git a/include/openPMD/UnitDimension.hpp b/include/openPMD/UnitDimension.hpp index e1773fc7a6..480901c0ca 100644 --- a/include/openPMD/UnitDimension.hpp +++ b/include/openPMD/UnitDimension.hpp @@ -54,10 +54,10 @@ namespace unit_representations using AsArrays = std::vector; auto asArray(AsMap const &) -> AsArray; - auto asMap(AsArray const &) -> AsMap; + auto asMap(AsArray const &, bool skip_zeros = true) -> AsMap; auto asArrays(AsMaps const &) -> AsArrays; - auto asMaps(AsArrays const &) -> AsMaps; + auto asMaps(AsArrays const &, bool skip_zeros = true) -> AsMaps; namespace auxiliary { diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 5f7c5c34ef..c5cdefe483 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -184,7 +184,6 @@ double Mesh::gridUnitSI() const Mesh &Mesh::setGridUnitSI(double gusi) { - setAttribute("gridUnitSI", gusi); if (auto standard = IOHandler()->m_standard; standard >= OpenpmdStandard::v_2_0_0) { @@ -195,12 +194,13 @@ Mesh &Mesh::setGridUnitSI(double gusi) "specify the gridUnitSI per axis (ref.: " "https://github.com/openPMD/openPMD-standard/pull/193).\n"; } + setAttribute("gridUnitSI", gusi); return *this; } -Mesh &Mesh::setGridUnitSI(std::vector gusi) +Mesh &Mesh::setGridUnitSI(std::vector const &gusi) { - return setGridUnitSIPerDimension(std::move(gusi)); + return setGridUnitSIPerDimension(gusi); } namespace @@ -247,7 +247,7 @@ std::vector Mesh::gridUnitSIPerDimension() const } } -Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) +Mesh &Mesh::setGridUnitSIPerDimension(std::vector const &gridUnitSI) { if (auto standard = IOHandler()->m_standard; standard < OpenpmdStandard::v_2_0_0) @@ -260,7 +260,7 @@ Mesh &Mesh::setGridUnitSIPerDimension(std::vector gridUnitSI) "openPMD 2.0. Either upgrade the file to openPMD >= 2.0 " "or specify a scalar that applies to all axes."); } - setAttribute("gridUnitSI", std::move(gridUnitSI)); + setAttribute("gridUnitSI", gridUnitSI); return *this; } @@ -276,6 +276,12 @@ Mesh &Mesh::setUnitDimension(unit_representations::AsMap const &udim) return *this; } +Mesh &Mesh::setUnitDimension(unit_representations::AsArray const &udim) +{ + return setUnitDimension( + unit_representations::asMap(udim, /* skip_zeros = */ false)); +} + Mesh &Mesh::setGridUnitDimension(unit_representations::AsMaps const &udims) { auto rawGridUnitDimension = [this]() { @@ -300,6 +306,12 @@ Mesh &Mesh::setGridUnitDimension(unit_representations::AsMaps const &udims) return *this; } +Mesh &Mesh::setGridUnitDimension(unit_representations::AsArrays const &udims) +{ + return setGridUnitDimension( + unit_representations::asMaps(udims, /* skip_zeros = */ false)); +} + unit_representations::AsArrays Mesh::gridUnitDimension() const { if (containsAttribute("gridUnitDimension")) diff --git a/src/Record.cpp b/src/Record.cpp index ad3771bdfe..7d41fce5c2 100644 --- a/src/Record.cpp +++ b/src/Record.cpp @@ -43,6 +43,11 @@ Record &Record::setUnitDimension(unit_representations::AsMap const &udim) } return *this; } +Record &Record::setUnitDimension(unit_representations::AsArray const &udim) +{ + return setUnitDimension( + unit_representations::asMap(udim, /* skip_zeros = */ false)); +} void Record::flush_impl( std::string const &name, internal::FlushParams const &flushParams) diff --git a/src/UnitDimension.cpp b/src/UnitDimension.cpp index 103fdea6c5..3c8c7755a3 100644 --- a/src/UnitDimension.cpp +++ b/src/UnitDimension.cpp @@ -10,12 +10,12 @@ auto asArray(AsMap const &udim) -> AsArray auxiliary::fromMapOfUnitDimension(res.data(), udim); return res; } -auto asMap(AsArray const &array) -> AsMap +auto asMap(AsArray const &array, bool skip_zeros) -> AsMap { AsMap udim; for (size_t i = 0; i < array.size(); ++i) { - if (array[i] != 0) + if (!skip_zeros || array[i] != 0) { udim[static_cast(i)] = array[i]; } @@ -33,14 +33,15 @@ auto asArrays(AsMaps const &vec) -> AsArrays }); return res; } -auto asMaps(AsArrays const &vec) -> AsMaps +auto asMaps(AsArrays const &vec, bool skip_zeros) -> AsMaps { AsMaps res; res.reserve(vec.size()); std::transform( - vec.begin(), vec.end(), std::back_inserter(res), [](auto const &array) { - return asMap(array); - }); + vec.begin(), + vec.end(), + std::back_inserter(res), + [&](auto const &array) { return asMap(array, skip_zeros); }); return res; } diff --git a/src/binding/python/Mesh.cpp b/src/binding/python/Mesh.cpp index 0346a68dfd..bdc6f182f6 100644 --- a/src/binding/python/Mesh.cpp +++ b/src/binding/python/Mesh.cpp @@ -21,6 +21,7 @@ #include "openPMD/Mesh.hpp" #include "openPMD/Error.hpp" #include "openPMD/IO/AbstractIOHandler.hpp" +#include "openPMD/UnitDimension.hpp" #include "openPMD/backend/Attributable.hpp" #include "openPMD/backend/BaseRecord.hpp" #include "openPMD/backend/MeshRecordComponent.hpp" @@ -61,13 +62,31 @@ void init_Mesh(py::module &m) .def_property( "unit_dimension", &Mesh::unitDimension, - &Mesh::setUnitDimension, + [](Mesh &self, + std::variant< + unit_representations::AsMap, + unit_representations::AsArray> const &arg) -> Mesh & { + return std::visit( + [&](auto const &arg_resolved) -> Mesh & { + return self.setUnitDimension(arg_resolved); + }, + arg); + }, python::doc_unit_dimension) .def_property( "grid_unit_dimension", &Mesh::gridUnitDimension, - &Mesh::setGridUnitDimension, + [](Mesh &self, + std::variant< + unit_representations::AsMaps, + unit_representations::AsArrays> const &arg) -> Mesh & { + return std::visit( + [&](auto const &arg_resolved) -> Mesh & { + return self.setGridUnitDimension(arg_resolved); + }, + arg); + }, python::doc_mesh_unit_dimension) .def_property( @@ -124,11 +143,11 @@ void init_Mesh(py::module &m) return return_t(self.gridUnitSIPerDimension()); } }, - [](Mesh &self, std::variant> arg) { + [](Mesh &self, + std::variant> const &arg) -> Mesh & { return std::visit( - [&](auto &&arg_resolved) { - return self.setGridUnitSI( - static_cast(arg_resolved)); + [&](auto const &arg_resolved) -> Mesh & { + return self.setGridUnitSI(arg_resolved); }, arg); }, @@ -159,7 +178,10 @@ Ref.: https://github.com/openPMD/openPMD-standard/pull/193)"[1]) &Mesh::setTimeOffset) // TODO remove in future versions (deprecated) - .def("set_unit_dimension", &Mesh::setUnitDimension) + .def( + "set_unit_dimension", + py::overload_cast( + &Mesh::setUnitDimension)) .def( "set_geometry", py::overload_cast(&Mesh::setGeometry)) diff --git a/src/binding/python/Record.cpp b/src/binding/python/Record.cpp index b4f732a83d..0d8a0e94bb 100644 --- a/src/binding/python/Record.cpp +++ b/src/binding/python/Record.cpp @@ -20,6 +20,7 @@ */ #include "openPMD/Record.hpp" #include "openPMD/RecordComponent.hpp" +#include "openPMD/UnitDimension.hpp" #include "openPMD/backend/Attributable.hpp" #include "openPMD/backend/BaseRecord.hpp" @@ -50,7 +51,16 @@ void init_Record(py::module &m) .def_property( "unit_dimension", &Record::unitDimension, - &Record::setUnitDimension, + [](Record &self, + std::variant< + unit_representations::AsMap, + unit_representations::AsArray> const &arg) -> Record & { + return std::visit( + [&](auto const &arg_resolved) -> Record & { + return self.setUnitDimension(arg_resolved); + }, + arg); + }, python::doc_unit_dimension) .def_property( @@ -67,7 +77,10 @@ void init_Record(py::module &m) &Record::setTimeOffset) // TODO remove in future versions (deprecated) - .def("set_unit_dimension", &Record::setUnitDimension) + .def( + "set_unit_dimension", + py::overload_cast( + &Record::setUnitDimension)) .def("set_time_offset", &Record::setTimeOffset) .def("set_time_offset", &Record::setTimeOffset) .def("set_time_offset", &Record::setTimeOffset); diff --git a/src/binding/python/UnitDimension.cpp b/src/binding/python/UnitDimension.cpp index eb32e87de1..abd119c5b3 100644 --- a/src/binding/python/UnitDimension.cpp +++ b/src/binding/python/UnitDimension.cpp @@ -43,7 +43,13 @@ void init_UnitDimension(py::module &m) return static_cast(idx); }) .def("as_array", &unit_representations::asArray) - .def("as_map", &unit_representations::asMap) + .def( + "as_map", + &unit_representations::asMap, + py::arg("skip_zeros") = true) .def("as_arrays", &unit_representations::asArrays) - .def("as_maps", &unit_representations::asMaps); + .def( + "as_maps", + &unit_representations::asMaps, + py::arg("skip_zeros") = true); } From 9b4dd8536afc637000f3fdcc02df358fd0631588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 4 Nov 2024 16:57:35 +0100 Subject: [PATCH 21/22] CI fixes --- test/python/unittest/API/APITest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/python/unittest/API/APITest.py b/test/python/unittest/API/APITest.py index d866091571..3cc5460929 100644 --- a/test/python/unittest/API/APITest.py +++ b/test/python/unittest/API/APITest.py @@ -499,8 +499,6 @@ def unsupported_in_1_1(): {io.Unit_Dimension.L: 1} for _ in range(3)]) read_1_1.close() - - def makeConstantRoundTrip(self, file_ending): # write series = io.Series( From e376d2f90b62f271cbce84092c77b45157ee7e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20P=C3=B6schel?= Date: Mon, 4 Nov 2024 17:46:50 +0100 Subject: [PATCH 22/22] Extend Python test --- test/python/unittest/API/APITest.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/python/unittest/API/APITest.py b/test/python/unittest/API/APITest.py index 3cc5460929..7151f88063 100644 --- a/test/python/unittest/API/APITest.py +++ b/test/python/unittest/API/APITest.py @@ -419,6 +419,7 @@ def testOpenPMD_2_0(self): E = meshes["E"] E.reset_dataset(io.Dataset(io.Datatype.DOUBLE, [10, 10, 10])) + E.axis_labels = ["x", "y", "z"] E.grid_unit_SI = [1, 2, 3] E.grid_unit_dimension = [ {io.Unit_Dimension.L: 1}, @@ -428,9 +429,9 @@ def testOpenPMD_2_0(self): B = meshes["B"] B.reset_dataset(io.Dataset(io.Datatype.DOUBLE, [10, 10, 10])) + B.axis_labels = ["x", "y", "z"] # This is deprecated for openPMD 2.0, a warning will be printed. B.grid_unit_SI = 3 - B.grid_unit_dimension = [{io.Unit_Dimension.L: 1} for _ in range(3)] B.make_constant(18) write_2_0.close() @@ -449,6 +450,7 @@ def testOpenPMD_2_0(self): B = meshes["B"] # Will return a list due to openPMD standard being set to 2.0.0 self.assertEqual(B.grid_unit_SI, [3]) + # If the attribute is not defined, the mesh is implicitly spatial self.assertEqual(io.Unit_Dimension.as_maps(B.grid_unit_dimension), [ {io.Unit_Dimension.L: 1} for _ in range(3)]) read_2_0.close() @@ -462,8 +464,9 @@ def testOpenPMD_2_0(self): def unsupported_in_1_1(): E.grid_unit_SI = [1, 2, 3] - self.assertRaises( - io.ErrorIllegalInOpenPMDStandard, unsupported_in_1_1) + # self.assertRaises( + # io.ErrorIllegalInOpenPMDStandard, unsupported_in_1_1) + E.axis_labels = ["x", "y", "z"] E.grid_unit_dimension = [ {io.Unit_Dimension.L: 1}, {io.Unit_Dimension.L: 1}, @@ -472,9 +475,9 @@ def unsupported_in_1_1(): B = meshes["B"] B.reset_dataset(io.Dataset(io.Datatype.DOUBLE, [10, 10, 10])) + B.axis_labels = ["x", "y", "z"] # This is deprecated for openPMD 2.0, a warning will be printed. B.grid_unit_SI = 3 - B.grid_unit_dimension = [{io.Unit_Dimension.L: 1} for _ in range(3)] B.make_constant(18) write_1_1.close() @@ -495,6 +498,7 @@ def unsupported_in_1_1(): B = meshes["B"] # Will return a scalar due to openPMD standard being set to 2.0.0 self.assertEqual(B.grid_unit_SI, 3) + # If the attribute is not defined, the mesh is implicitly spatial self.assertEqual(io.Unit_Dimension.as_maps(B.grid_unit_dimension), [ {io.Unit_Dimension.L: 1} for _ in range(3)]) read_1_1.close()