diff --git a/CMakeLists.txt b/CMakeLists.txt
index 32007025a6..f2fd9ce644 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -390,6 +390,7 @@ include(${openPMD_SOURCE_DIR}/cmake/dependencies/pybind11.cmake)
set(CORE_SOURCE
src/config.cpp
src/ChunkInfo.cpp
+ src/CustomHierarchy.cpp
src/Dataset.cpp
src/Datatype.cpp
src/Error.cpp
@@ -560,6 +561,7 @@ if(openPMD_HAVE_PYTHON)
src/binding/python/Attributable.cpp
src/binding/python/BaseRecordComponent.cpp
src/binding/python/ChunkInfo.cpp
+ src/binding/python/CustomHierarchy.cpp
src/binding/python/Dataset.cpp
src/binding/python/Datatype.cpp
src/binding/python/Error.cpp
@@ -726,6 +728,7 @@ set(openPMD_PYTHON_EXAMPLE_NAMES
11_particle_dataframe
12_span_write
13_write_dynamic_configuration
+ 14_custom_hierarchy
)
if(openPMD_USE_INVASIVE_TESTS)
diff --git a/examples/14_custom_hierarchy.py b/examples/14_custom_hierarchy.py
new file mode 100755
index 0000000000..b3eff208a9
--- /dev/null
+++ b/examples/14_custom_hierarchy.py
@@ -0,0 +1,48 @@
+import numpy as np
+import openpmd_api as io
+
+
+def main():
+ if "bp" in io.file_extensions:
+ filename = "../samples/custom_hierarchy.bp"
+ else:
+ filename = "../samples/custom_hierarchy.json"
+ s = io.Series(filename, io.Access.create)
+ it = s.write_iterations()[100]
+
+ # write openPMD part
+ temp = it.meshes["temperature"]
+ temp.axis_labels = ["x", "y"]
+ temp.unit_dimension = {io.Unit_Dimension.T: 1}
+ temp.position = [0.5, 0.5]
+ temp.grid_spacing = [1, 1]
+ temp.grid_global_offset = [0, 0]
+ temp.reset_dataset(io.Dataset(np.dtype("double"), [5, 5]))
+ temp[()] = np.zeros((5, 5))
+
+ # write NeXus part
+ nxentry = it["Scan"]
+ nxentry.set_attribute("NX_class", "NXentry")
+ nxentry.set_attribute("default", "data")
+
+ data = nxentry["data"]
+ data.set_attribute("NX_class", "NXdata")
+ data.set_attribute("signal", "counts")
+ data.set_attribute("axes", ["two_theta"])
+ data.set_attribute("two_theta_indices", [0])
+
+ counts = data.as_container_of_datasets()["counts"]
+ counts.set_attribute("units", "counts")
+ counts.set_attribute("long_name", "photodiode counts")
+ counts.reset_dataset(io.Dataset(np.dtype("int"), [15]))
+ counts[()] = np.zeros(15, dtype=np.dtype("int"))
+
+ two_theta = data.as_container_of_datasets()["two_theta"]
+ two_theta.set_attribute("units", "degrees")
+ two_theta.set_attribute("long_name", "two_theta (degrees)")
+ two_theta.reset_dataset(io.Dataset(np.dtype("double"), [15]))
+ two_theta[()] = np.zeros(15)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/include/openPMD/CustomHierarchy.hpp b/include/openPMD/CustomHierarchy.hpp
new file mode 100644
index 0000000000..dc23a97e64
--- /dev/null
+++ b/include/openPMD/CustomHierarchy.hpp
@@ -0,0 +1,256 @@
+/* Copyright 2023 Franz Poeschel
+ *
+ * This file is part of openPMD-api.
+ *
+ * openPMD-api is free software: you can redistribute it and/or modify
+ * it under the terms of of either the GNU General Public License or
+ * the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * openPMD-api is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License and the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * and the GNU Lesser General Public License along with openPMD-api.
+ * If not, see .
+ */
+#pragma once
+
+#include "openPMD/IO/AbstractIOHandler.hpp"
+#include "openPMD/Mesh.hpp"
+#include "openPMD/ParticleSpecies.hpp"
+#include "openPMD/RecordComponent.hpp"
+#include "openPMD/backend/Container.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace openPMD
+{
+class CustomHierarchy;
+namespace internal
+{
+ enum class ContainedType
+ {
+ Group,
+ Mesh,
+ Particle
+ };
+ struct MeshesParticlesPath
+ {
+ std::regex meshRegex;
+ std::set collectNewMeshesPaths;
+ std::regex particleRegex;
+ std::set collectNewParticlesPaths;
+
+ /*
+ * These values decide which path will be returned upon use of the
+ * shorthand notation s.iterations[0].meshes or .particles.
+ *
+ */
+ std::string m_defaultMeshesPath = "meshes";
+ std::string m_defaultParticlesPath = "particles";
+
+ explicit MeshesParticlesPath() = default;
+ MeshesParticlesPath(
+ std::vector const &meshes,
+ std::vector const &particles);
+ MeshesParticlesPath(Series const &);
+
+ [[nodiscard]] ContainedType
+ determineType(std::vector const &path) const;
+ [[nodiscard]] bool
+ isParticleContainer(std::vector const &path) const;
+ [[nodiscard]] bool
+ isMeshContainer(std::vector const &path) const;
+ };
+
+ struct CustomHierarchyData
+ : ContainerData
+ , ContainerData
+ , ContainerData
+ , ContainerData
+ {
+ explicit CustomHierarchyData();
+
+ void syncAttributables();
+
+#if 0
+ inline Container customHierarchiesWrapped()
+ {
+ Container res;
+ res.setData(
+ {static_cast *>(this),
+ [](auto const *) {}});
+ return res;
+ }
+#endif
+ inline Container embeddedDatasetsWrapped()
+ {
+ Container res;
+ res.setData(
+ {static_cast *>(this),
+ [](auto const *) {}});
+ return res;
+ }
+ inline Container embeddedMeshesWrapped()
+ {
+ Container res;
+ res.setData(
+ {static_cast *>(this),
+ [](auto const *) {}});
+ return res;
+ }
+
+ inline Container embeddedParticlesWrapped()
+ {
+ Container res;
+ res.setData(
+ {static_cast *>(this),
+ [](auto const *) {}});
+ return res;
+ }
+
+#if 0
+ inline Container::InternalContainer &
+ customHierarchiesInternal()
+ {
+ return static_cast *>(this)
+ ->m_container;
+ }
+#endif
+ inline Container::InternalContainer &
+ embeddedDatasetsInternal()
+ {
+ return static_cast *>(this)
+ ->m_container;
+ }
+ inline Container::InternalContainer &embeddedMeshesInternal()
+ {
+ return static_cast *>(this)->m_container;
+ }
+
+ inline Container::InternalContainer &
+ embeddedParticlesInternal()
+ {
+ return static_cast *>(this)
+ ->m_container;
+ }
+ };
+} // namespace internal
+
+template
+class ConversibleContainer : public Container
+{
+ template
+ friend class ConversibleContainer;
+
+protected:
+ using Container_t = Container;
+ using Data_t = internal::CustomHierarchyData;
+ static_assert(
+ std::is_base_of_v);
+
+ ConversibleContainer(Attributable::NoInit)
+ : Container_t(Attributable::NoInit{})
+ {}
+
+ std::shared_ptr m_customHierarchyData;
+
+ [[nodiscard]] Data_t &get()
+ {
+ return *m_customHierarchyData;
+ }
+ [[nodiscard]] Data_t const &get() const
+ {
+ return *m_customHierarchyData;
+ }
+
+ inline void setData(std::shared_ptr data)
+ {
+ m_customHierarchyData = data;
+ Container_t::setData(std::move(data));
+ }
+
+public:
+ template
+ auto asContainerOf() -> ConversibleContainer
+ {
+ if constexpr (
+ std::is_same_v ||
+ std::is_same_v ||
+ std::is_same_v ||
+ std::is_same_v)
+ {
+ ConversibleContainer res(Attributable::NoInit{});
+ res.setData(m_customHierarchyData);
+ return res;
+ }
+ else
+ {
+ static_assert(
+ auxiliary::dependent_false_v,
+ "[CustomHierarchy::asContainerOf] Type parameter must be "
+ "one of: CustomHierarchy, RecordComponent, Mesh, "
+ "ParticleSpecies.");
+ }
+ }
+};
+
+class CustomHierarchy : public ConversibleContainer
+{
+ friend class Iteration;
+ friend class Container;
+
+private:
+ using Container_t = Container;
+ using Parent_t = ConversibleContainer;
+ using Data_t = typename Parent_t::Data_t;
+
+ using EraseStaleMeshes = internal::EraseStaleEntries>;
+ using EraseStaleParticles =
+ internal::EraseStaleEntries>;
+ void readNonscalarMesh(EraseStaleMeshes &map, std::string const &name);
+ void readScalarMesh(EraseStaleMeshes &map, std::string const &name);
+ void readParticleSpecies(EraseStaleParticles &map, std::string const &name);
+
+protected:
+ CustomHierarchy();
+ CustomHierarchy(NoInit);
+
+ void read(internal::MeshesParticlesPath const &);
+ void read(
+ internal::MeshesParticlesPath const &,
+ std::vector ¤tPath);
+
+ void flush_internal(
+ internal::FlushParams const &,
+ internal::MeshesParticlesPath &,
+ std::vector currentPath);
+ void flush(std::string const &path, internal::FlushParams const &) override;
+
+ /**
+ * @brief Link with parent.
+ *
+ * @param w The Writable representing the parent.
+ */
+ void linkHierarchy(Writable &w) override;
+
+public:
+ CustomHierarchy(CustomHierarchy const &other) = default;
+ CustomHierarchy(CustomHierarchy &&other) = default;
+
+ CustomHierarchy &operator=(CustomHierarchy const &) = default;
+ CustomHierarchy &operator=(CustomHierarchy &&) = default;
+};
+} // namespace openPMD
diff --git a/include/openPMD/IO/JSON/JSONIOHandlerImpl.hpp b/include/openPMD/IO/JSON/JSONIOHandlerImpl.hpp
index 3691055985..ce33a230db 100644
--- a/include/openPMD/IO/JSON/JSONIOHandlerImpl.hpp
+++ b/include/openPMD/IO/JSON/JSONIOHandlerImpl.hpp
@@ -398,7 +398,8 @@ class JSONIOHandlerImpl : public AbstractIOHandlerImpl
// make sure that the given path exists in proper form in
// the passed json value
- static void ensurePath(nlohmann::json *json, std::string const &path);
+ static void
+ ensurePath(nlohmann::json *json, std::string const &path, Access);
// In order not to insert the same file name into the data structures
// with a new pointer (e.g. when reopening), search for a possibly
diff --git a/include/openPMD/Iteration.hpp b/include/openPMD/Iteration.hpp
index 43ee1084bb..a79fc1a56d 100644
--- a/include/openPMD/Iteration.hpp
+++ b/include/openPMD/Iteration.hpp
@@ -20,6 +20,7 @@
*/
#pragma once
+#include "openPMD/CustomHierarchy.hpp"
#include "openPMD/IterationEncoding.hpp"
#include "openPMD/Mesh.hpp"
#include "openPMD/ParticleSpecies.hpp"
@@ -98,7 +99,7 @@ namespace internal
BeginStep beginStep = BeginStepTypes::DontBeginStep{};
};
- class IterationData : public AttributableData
+ class IterationData : public CustomHierarchyData
{
public:
/*
@@ -142,10 +143,22 @@ namespace internal
* @see
* https://github.com/openPMD/openPMD-standard/blob/latest/STANDARD.md#required-attributes-for-the-basepath
*/
-class Iteration : public Attributable
+class Iteration : public CustomHierarchy
{
- template
- friend class Container;
+public:
+ using IterationIndex_t = uint64_t;
+
+ /*
+ * Some old compilers have trouble with befriending the entire Container
+ * template here, so we restrict it
+ * to Container, more is not needed anyway.
+ *
+ * E.g. on gcc-7:
+ * > error: specialization of 'openPMD::Container'
+ * > after instantiation
+ * > friend class Container;
+ */
+ friend class Container;
friend class Series;
friend class internal::AttributableData;
template
@@ -154,13 +167,17 @@ class Iteration : public Attributable
friend class StatefulIterator;
friend class StatefulSnapshotsContainer;
-public:
Iteration(Iteration const &) = default;
Iteration(Iteration &&) = default;
Iteration &operator=(Iteration const &) = default;
Iteration &operator=(Iteration &&) = default;
- using IterationIndex_t = uint64_t;
+ // These use the openPMD Container class mainly for consistency.
+ // But they are in fact only aliases that don't actually exist
+ // in the backend.
+ // Hence meshes.written() and particles.written() will always be false.
+ Container meshes{};
+ Container particles{};
/**
* @tparam T Floating point type of user-selected precision (e.g. float,
@@ -268,9 +285,6 @@ class Iteration : public Attributable
[[deprecated("This attribute is no longer set by the openPMD-api.")]] bool
closedByWriter() const;
- Container meshes{};
- Container particles{}; // particleSpecies?
-
virtual ~Iteration() = default;
private:
@@ -297,14 +311,25 @@ class Iteration : public Attributable
inline void setData(std::shared_ptr data)
{
m_iterationData = std::move(data);
- Attributable::setData(m_iterationData);
+ CustomHierarchy::setData(m_iterationData);
}
void flushFileBased(
std::string const &, IterationIndex_t, internal::FlushParams const &);
void flushGroupBased(IterationIndex_t, internal::FlushParams const &);
void flushVariableBased(IterationIndex_t, internal::FlushParams const &);
- void flush(internal::FlushParams const &);
+ /*
+ * Named flushIteration instead of flush to avoid naming
+ * conflicts with overridden virtual flush from CustomHierarchy
+ * class.
+ */
+ void flushIteration(internal::FlushParams const &);
+
+ void sync_meshes_and_particles_from_alias_to_subgroups(
+ internal::MeshesParticlesPath const &);
+ void sync_meshes_and_particles_from_subgroups_to_alias(
+ internal::MeshesParticlesPath const &);
+
void deferParseAccess(internal::DeferredParseAccess);
/*
* Control flow for runDeferredParseAccess(), readFileBased(),
@@ -334,8 +359,6 @@ class Iteration : public Attributable
void readGorVBased(
std::string const &groupPath, internal::BeginStep const &beginStep);
void read_impl(std::string const &groupPath);
- void readMeshes(std::string const &meshesPath);
- void readParticles(std::string const &particlesPath);
/**
* Status after beginning an IO step. Currently includes:
@@ -416,12 +439,22 @@ class Iteration : public Attributable
*/
void setStepStatus(StepStatus);
+ /*
+ * @brief Check recursively whether this Iteration is dirty.
+ * It is dirty if any attribute or dataset is read from or written to
+ * the backend.
+ *
+ * @return true If dirty.
+ * @return false Otherwise.
+ */
+ bool dirtyRecursive() const;
+
/**
* @brief Link with parent.
*
* @param w The Writable representing the parent.
*/
- virtual void linkHierarchy(Writable &w);
+ void linkHierarchy(Writable &w);
/**
* @brief Access an iteration in read mode that has potentially not been
diff --git a/include/openPMD/Mesh.hpp b/include/openPMD/Mesh.hpp
index 77ef8b2886..edbd6ff6ff 100644
--- a/include/openPMD/Mesh.hpp
+++ b/include/openPMD/Mesh.hpp
@@ -41,6 +41,7 @@ class Mesh : public BaseRecord
{
friend class Container;
friend class Iteration;
+ friend class CustomHierarchy;
public:
Mesh(Mesh const &) = default;
diff --git a/include/openPMD/ParticleSpecies.hpp b/include/openPMD/ParticleSpecies.hpp
index af7aa50375..9f454a0ed5 100644
--- a/include/openPMD/ParticleSpecies.hpp
+++ b/include/openPMD/ParticleSpecies.hpp
@@ -37,6 +37,7 @@ class ParticleSpecies : public Container
friend class Iteration;
template
friend T &internal::makeOwning(T &self, Series);
+ friend class CustomHierarchy;
public:
ParticlePatches particlePatches;
diff --git a/include/openPMD/RecordComponent.hpp b/include/openPMD/RecordComponent.hpp
index ee29a6d7fa..67322a7d36 100644
--- a/include/openPMD/RecordComponent.hpp
+++ b/include/openPMD/RecordComponent.hpp
@@ -135,6 +135,7 @@ class RecordComponent : public BaseRecordComponent
friend class MeshRecordComponent;
template
friend T &internal::makeOwning(T &self, Series);
+ friend class CustomHierarchy;
public:
enum class Allocation
@@ -487,8 +488,9 @@ class RecordComponent : public BaseRecordComponent
static constexpr char const *const SCALAR = "\vScalar";
protected:
- void flush(std::string const &, internal::FlushParams const &);
- void read(bool require_unit_si);
+ void flush(
+ std::string const &, internal::FlushParams const &, bool set_defaults);
+ void read(bool read_defaults);
private:
/**
@@ -536,7 +538,7 @@ OPENPMD_protected
BaseRecordComponent::setData(m_recordComponentData);
}
- void readBase(bool require_unit_si);
+ void readBase(bool read_defaults);
template
void verifyChunk(Offset const &, Extent const &) const;
diff --git a/include/openPMD/Series.hpp b/include/openPMD/Series.hpp
index 0a568ed559..2036ccb298 100644
--- a/include/openPMD/Series.hpp
+++ b/include/openPMD/Series.hpp
@@ -293,6 +293,7 @@ class Series : public Attributable
friend class internal::SeriesData;
friend class internal::AttributableData;
friend class StatefulSnapshotsContainer;
+ friend class CustomHierarchy;
public:
explicit Series();
@@ -446,6 +447,7 @@ class Series : public Attributable
* basePath
.
*/
std::string meshesPath() const;
+ std::vector meshesPaths() const;
/** Set the path to mesh
* records, relative(!) to basePath
.
@@ -456,6 +458,7 @@ class Series : public Attributable
* @return Reference to modified series.
*/
Series &setMeshesPath(std::string const &meshesPath);
+ Series &setMeshesPath(std::vector const &meshesPath);
/**
* @throw no_such_attribute_error If optional attribute is not present.
@@ -489,6 +492,7 @@ class Series : public Attributable
* basePath
.
*/
std::string particlesPath() const;
+ std::vector particlesPaths() const;
/** Set the path to groups for each particle
* species, relative(!) to basePath
.
@@ -499,6 +503,7 @@ class Series : public Attributable
* @return Reference to modified series.
*/
Series &setParticlesPath(std::string const &particlesPath);
+ Series &setParticlesPath(std::vector const &particlesPath);
/**
* @throw no_such_attribute_error If optional attribute is not present.
@@ -899,8 +904,6 @@ OPENPMD_private
iterations_iterator end,
internal::FlushParams const &flushParams,
bool flushIOHandler = true);
- void flushMeshesPath();
- void flushParticlesPath();
void flushRankTable();
/* Parameter `read_only_this_single_iteration` used for reopening an
* Iteration after closing it.
diff --git a/include/openPMD/backend/Attributable.hpp b/include/openPMD/backend/Attributable.hpp
index 732b2d1b5c..791ee592e4 100644
--- a/include/openPMD/backend/Attributable.hpp
+++ b/include/openPMD/backend/Attributable.hpp
@@ -50,6 +50,7 @@ class AbstractFilePosition;
class Attributable;
class Iteration;
class Series;
+class CustomHierarchy;
namespace internal
{
@@ -59,6 +60,7 @@ namespace internal
class SharedAttributableData
{
friend class openPMD::Attributable;
+ friend class openPMD::CustomHierarchy;
public:
SharedAttributableData(AttributableData *);
@@ -104,6 +106,7 @@ namespace internal
class AttributableData : public std::shared_ptr
{
friend class openPMD::Attributable;
+ friend class openPMD::CustomHierarchy;
using SharedData_t = std::shared_ptr;
@@ -114,6 +117,17 @@ namespace internal
AttributableData(AttributableData &&) = delete;
virtual ~AttributableData() = default;
+ inline std::shared_ptr &
+ asSharedPtrOfAttributable()
+ {
+ return *this;
+ }
+ inline std::shared_ptr const &
+ asSharedPtrOfAttributable() const
+ {
+ return *this;
+ }
+
AttributableData &operator=(AttributableData const &) = delete;
AttributableData &operator=(AttributableData &&) = delete;
@@ -158,6 +172,7 @@ namespace internal
class BaseRecordData;
class RecordComponentData;
+ struct CustomHierarchyData;
/*
* Internal function to turn a handle into an owning handle that will keep
@@ -209,6 +224,8 @@ class Attributable
friend T &internal::makeOwning(T &self, Series);
friend class StatefulSnapshotsContainer;
friend class internal::AttributableData;
+ friend class CustomHierarchy;
+ friend struct internal::CustomHierarchyData;
protected:
// tag for internal constructor
diff --git a/include/openPMD/backend/Attribute.hpp b/include/openPMD/backend/Attribute.hpp
index 90ae80582b..e0a0e1fa57 100644
--- a/include/openPMD/backend/Attribute.hpp
+++ b/include/openPMD/backend/Attribute.hpp
@@ -293,6 +293,25 @@ namespace detail
}
}
}
+ // conversion cast: turn a 1-element vector into a single value
+ else if constexpr (auxiliary::IsVector_v)
+ {
+ if constexpr (std::is_convertible_v)
+ {
+ if (pv->size() != 1)
+ {
+ return {std::runtime_error(
+ "getCast: vector to scalar conversion requires "
+ "single-element vectors")};
+ }
+ return {U(*pv->begin())};
+ }
+ else
+ {
+ return {std::runtime_error(
+ "getCast: no vector to scalar conversion possible.")};
+ }
+ }
else
{
return {std::runtime_error("getCast: no cast possible.")};
diff --git a/include/openPMD/backend/BaseRecord.hpp b/include/openPMD/backend/BaseRecord.hpp
index 3eeca4d8d8..13f703a5d6 100644
--- a/include/openPMD/backend/BaseRecord.hpp
+++ b/include/openPMD/backend/BaseRecord.hpp
@@ -232,6 +232,7 @@ class BaseRecord
private:
using T_Self = BaseRecord;
+ friend class CustomHierarchy;
friend class Iteration;
friend class ParticleSpecies;
friend class PatchRecord;
diff --git a/include/openPMD/backend/Container.hpp b/include/openPMD/backend/Container.hpp
index 8dda5b992b..a3d1a6a934 100644
--- a/include/openPMD/backend/Container.hpp
+++ b/include/openPMD/backend/Container.hpp
@@ -57,11 +57,26 @@ namespace traits
};
} // namespace traits
+class CustomHierarchy;
+
namespace internal
{
+ template
+ constexpr inline bool isDerivedFromAttributable =
+ std::is_base_of_v;
+
+ /*
+ * Opt out from this check due to the recursive definition of
+ * class CustomHierarchy : public Container{ ... };
+ * Cannot check this while CustomHierarchy is still an incomplete type.
+ */
+ template <>
+ constexpr inline bool isDerivedFromAttributable = true;
+
class SeriesData;
template
class EraseStaleEntries;
+ struct CustomHierarchyData;
template <
typename T,
@@ -103,7 +118,7 @@ template <
class Container : virtual public Attributable
{
static_assert(
- std::is_base_of::value,
+ internal::isDerivedFromAttributable,
"Type of container element must be derived from Writable");
friend class Iteration;
@@ -114,6 +129,9 @@ class Container : virtual public Attributable
template
friend class internal::EraseStaleEntries;
friend class StatefulIterator;
+ friend class SeriesIterator;
+ friend struct internal::CustomHierarchyData;
+ friend class CustomHierarchy;
protected:
using ContainerData = internal::ContainerData;
diff --git a/include/openPMD/backend/Writable.hpp b/include/openPMD/backend/Writable.hpp
index bfb9c67e03..c0ea88ef67 100644
--- a/include/openPMD/backend/Writable.hpp
+++ b/include/openPMD/backend/Writable.hpp
@@ -105,6 +105,7 @@ class Writable final
friend void debug::printDirty(Series const &);
friend struct Parameter;
friend struct Parameter;
+ friend class CustomHierarchy;
private:
Writable(internal::AttributableData *);
diff --git a/include/openPMD/binding/python/Common.hpp b/include/openPMD/binding/python/Common.hpp
index c72d72ce83..2c09338cc1 100644
--- a/include/openPMD/binding/python/Common.hpp
+++ b/include/openPMD/binding/python/Common.hpp
@@ -8,6 +8,7 @@
*/
#pragma once
+#include "openPMD/CustomHierarchy.hpp"
#include "openPMD/Iteration.hpp"
#include "openPMD/Mesh.hpp"
#include "openPMD/ParticlePatches.hpp"
@@ -50,6 +51,7 @@ using PyPatchRecordComponentContainer = Container;
using PyBaseRecordRecordComponent = BaseRecord;
using PyBaseRecordMeshRecordComponent = BaseRecord;
using PyBaseRecordPatchRecordComponent = BaseRecord;
+using PyCustomHierarchyContainer = Container;
PYBIND11_MAKE_OPAQUE(PyIterationContainer)
PYBIND11_MAKE_OPAQUE(PyMeshContainer)
PYBIND11_MAKE_OPAQUE(PyPartContainer)
@@ -61,3 +63,4 @@ PYBIND11_MAKE_OPAQUE(PyMeshRecordComponentContainer)
PYBIND11_MAKE_OPAQUE(PyPatchRecordComponentContainer)
PYBIND11_MAKE_OPAQUE(PyBaseRecordRecordComponent)
PYBIND11_MAKE_OPAQUE(PyBaseRecordPatchRecordComponent)
+PYBIND11_MAKE_OPAQUE(PyCustomHierarchyContainer)
diff --git a/src/CustomHierarchy.cpp b/src/CustomHierarchy.cpp
new file mode 100644
index 0000000000..70a4489f04
--- /dev/null
+++ b/src/CustomHierarchy.cpp
@@ -0,0 +1,663 @@
+/* Copyright 2023 Franz Poeschel
+ *
+ * This file is part of openPMD-api.
+ *
+ * openPMD-api is free software: you can redistribute it and/or modify
+ * it under the terms of of either the GNU General Public License or
+ * the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * openPMD-api is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License and the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * and the GNU Lesser General Public License along with openPMD-api.
+ * If not, see .
+ */
+
+#include "openPMD/CustomHierarchy.hpp"
+
+#include "openPMD/Dataset.hpp"
+#include "openPMD/Error.hpp"
+#include "openPMD/IO/AbstractIOHandler.hpp"
+#include "openPMD/IO/Access.hpp"
+#include "openPMD/IO/IOTask.hpp"
+#include "openPMD/Mesh.hpp"
+#include "openPMD/ParticleSpecies.hpp"
+#include "openPMD/RecordComponent.hpp"
+#include "openPMD/Series.hpp"
+#include "openPMD/auxiliary/StringManip.hpp"
+#include "openPMD/backend/Attributable.hpp"
+#include "openPMD/backend/BaseRecord.hpp"
+#include "openPMD/backend/MeshRecordComponent.hpp"
+#include "openPMD/backend/Writable.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include