diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index 1216659d..b8dd029f 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -16,6 +16,7 @@ PROJECT_NUMBER = "@PROJECT_VERSION@" # simplified expansion of the macro for documentation purposes MACRO_EXPANSION = YES PREDEFINED += "DEFINE_FIELD(name, storageObjectType, default_type, fieldPath, description)=/** description */ template inline std::unique_ptr> name() const;" +PREDEFINED += "DEFINE_REGISTERED_FIELD(name, registeredType, fieldPath, description)=/** description */ template inline std::shared_ptr name() const;" EXPAND_ONLY_PREDEF = YES diff --git a/docs/pages/devdocs/registered_types.dox b/docs/pages/devdocs/registered_types.dox index 6d4d8ae1..89260a20 100644 --- a/docs/pages/devdocs/registered_types.dox +++ b/docs/pages/devdocs/registered_types.dox @@ -56,35 +56,35 @@ * DEFINE_FIELD(getData, DatasetField, float, "data", The main data) * @endcode * + * 6. Similarly, we use the \ref DEFINE_REGISTERED_FIELD macro to define getter methods for other + * \ref AQNWB::NWB::RegisteredType "RegisteredType" objects that we own, such as a + * \ref AQNWB::NWB::ElectrodeTable "ElectrodeTable" that owns + * predefined \ref AQNWB::NWB::VectorData "VectorData" columns: + * @code + * DEFINE_REGISTERED_FIELD(readGroupNameColumn, VectorData, "group_name", "the name of the ElectrodeGroup") + * @endcode + * + * * \warning * To ensure proper function on read, the name of the class should match the name of the * ``neurodata_type`` as defined in the schema. Similarly, "my-namespace" should match * the name of the namespace in the schema (e.g., "core", "hdmf-common"). In this way * we can look up the corresponding class for an object in a file based on the * ``neurodata_type`` and ``namespace`` attributes stored in the file. - * - * \note * A special version of the ``REGISTER_SUBCLASS`` macro, called ``REGISTER_SUBCLASS_WITH_TYPENAME``, * allows setting the typename explicitly as a third argument. This is for the **special case** - * where we want to implement a class for a modified type that does not have its - * own `neurodata_type` in the NWB schema. An example is `ElectrodesTable` in NWB ` will be used on read. To support reading using the + * more specific types, we can use the \ref DEFINE_REGISTERED_FIELD macro to define + * read methods that will return the approbriate type, e.g.: + * + * \code + * DEFINE_REGISTERED_FIELD(readElectrodeTable, + * ElectrodeTable, + * ElectrodeTable::electrodeTablePath, + * "table with the extracellular electrodes") + * \endcode + * + * in \ref AQNWB::NWB::NWBFile "NWBFile" to read the \ref AQNWB::NWB::ElectrodeTable "ElectrodeTable", + * or + * + * \code + * DEFINE_REGISTERED_FIELD( + * readGroupNameColumn, + * VectorData, + * "group_name", + * "the name of the ElectrodeGroup this electrode is a part of") + * \endcode + * + * in the \ref AQNWB::NWB::ElectrodeTable "ElectrodeTable" to read the `group_name` column + * as `VectorData` with the data type already specified as `std::string` at compile time. + * * */ \ No newline at end of file diff --git a/docs/pages/userdocs/read.dox b/docs/pages/userdocs/read.dox index 10f27958..405d241d 100644 --- a/docs/pages/userdocs/read.dox +++ b/docs/pages/userdocs/read.dox @@ -477,9 +477,22 @@ * * \snippet tests/examples/test_ecephys_data_read.cpp example_read_new_io_snippet * - * \subsubsection read_design_example_read_posthoc_search Searching for Registered Type Objects (e.g.,ElectricalSeries) + * \subsubsection read_predefined_registered_field Reading known RegisteredType object * - * Using the \ref AQNWB::IO::BaseIO::findTypes "findType()" function of our I/O object we can + * When the path and type of objects is fixed in the schema (or we know them based on other conventions), + * then we can read the types directly from the file. E.g., here we first read the + * \ref AQNWB::NWB::NWBFile "NWBFile" directly, which we know exists at the root "/" + * of the file. We then read the \ref AQNWB::NWB::ElectrodeTable "ElectrodeTable" + * via the predefined \ref AQNWB::NWB::NWBFile::readElectrodeTable "NWBFile::readElectrodeTable" + * method. The advantage of this approach is that we do not need to manually specify paths + * or object types. Similarlry, when we read the `locations` columns, we do not need to + * specify the name or the data type to use. + * + * \snippet tests/examples/test_ecephys_data_read.cpp example_read_predefined_types + * + * \subsubsection read_design_example_read_posthoc_search Searching for Registered Type objects (e.g.,ElectricalSeries) + * + * When paths are not fixed, we can use the \ref AQNWB::IO::BaseIO::findTypes "findType()" function of our I/O object to * conveniently search for objects with a given type. * * \snippet tests/examples/test_ecephys_data_read.cpp example_search_types_snippet @@ -496,7 +509,7 @@ * \warning * The current implementation of \ref AQNWB::IO::BaseIO::findTypes "findType()" is * not aware of inheritance but searches for exact matches of types only. - * However, we can search for objects of multiple different times at the same by + * However, we can search for objects of multiple different times at the same time by * specifying multiple types to search for in our ``typesToSearch``. * * The returned ``std::unordered_map`` uses the full to object as key and the full diff --git a/src/io/hdf5/HDF5IO.cpp b/src/io/hdf5/HDF5IO.cpp index 2313b05f..efe51b00 100644 --- a/src/io/hdf5/HDF5IO.cpp +++ b/src/io/hdf5/HDF5IO.cpp @@ -1025,8 +1025,8 @@ HDF5IO::getStorageObjects(const std::string& path, if (objectType == StorageObjectType::Attribute || objectType == StorageObjectType::Undefined) { - SizeType numAttrs = group.getNumAttrs(); - for (SizeType i = 0; i < numAttrs; ++i) { + unsigned int numAttrs = static_cast(group.getNumAttrs()); + for (unsigned int i = 0; i < numAttrs; ++i) { H5::Attribute attr = group.openAttribute(i); objects.emplace_back(attr.getName(), StorageObjectType::Attribute); } @@ -1036,8 +1036,8 @@ HDF5IO::getStorageObjects(const std::string& path, || objectType == StorageObjectType::Undefined) { H5::DataSet dataset = m_file->openDataSet(path); - SizeType numAttrs = dataset.getNumAttrs(); - for (SizeType i = 0; i < numAttrs; ++i) { + unsigned int numAttrs = static_cast(dataset.getNumAttrs()); + for (unsigned int i = 0; i < numAttrs; ++i) { H5::Attribute attr = dataset.openAttribute(i); objects.emplace_back(attr.getName(), StorageObjectType::Attribute); } diff --git a/src/nwb/NWBFile.hpp b/src/nwb/NWBFile.hpp index 9234b69b..93e81b26 100644 --- a/src/nwb/NWBFile.hpp +++ b/src/nwb/NWBFile.hpp @@ -151,6 +151,48 @@ class NWBFile : public Container RecordingContainers* recordingContainers = nullptr, std::vector& containerIndexes = emptyContainerIndexes); + DEFINE_REGISTERED_FIELD(readElectrodeTable, + ElectrodeTable, + ElectrodeTable::electrodeTablePath, + "table with the extracellular electrodes") + + DEFINE_FIELD(readNWBVersion, + AttributeField, + std::string, + "nwb_version", + File version string) + + DEFINE_FIELD(readFileCreateDate, + DatasetField, + std::any, + "file_create_date", + A record of the date the file was created and of subsequent + modifications) + + DEFINE_FIELD(readIdentifier, + DatasetField, + std::string, + "identifier", + A unique text identifier for the file) + + DEFINE_FIELD(readSessionDescription, + DatasetField, + std::string, + "session_description", + A description of the experimental session and data in the file) + + DEFINE_FIELD(readSessionStartTime, + DatasetField, + std::any, + "session_start_time", + Date and time of the experiment or session start) + + DEFINE_FIELD(readTimestampsReferenceTime, + DatasetField, + std::any, + "timestamps_reference_time", + Date and time corresponding to time zero of all timestamps) + protected: /** * @brief Creates the default file structure. @@ -207,43 +249,6 @@ class NWBFile : public Container inline const static std::string acquisitionPath = "/acquisition"; static std::vector emptyContainerIndexes; - DEFINE_FIELD(readNWBVersion, - AttributeField, - std::string, - "nwb_version", - File version string) - - DEFINE_FIELD(readFileCreateDate, - DatasetField, - std::any, - "file_create_date", - A record of the date the file was created and of subsequent - modifications) - - DEFINE_FIELD(readIdentifier, - DatasetField, - std::string, - "identifier", - A unique text identifier for the file) - - DEFINE_FIELD(readSessionDescription, - DatasetField, - std::string, - "session_description", - A description of the experimental session and data in the file) - - DEFINE_FIELD(readSessionStartTime, - DatasetField, - std::any, - "session_start_time", - Date and time of the experiment or session start) - - DEFINE_FIELD(readTimestampsReferenceTime, - DatasetField, - std::any, - "timestamps_reference_time", - Date and time corresponding to time zero of all timestamps) - private: /** * @brief The ElectrodeTable for the file diff --git a/src/nwb/RegisteredType.hpp b/src/nwb/RegisteredType.hpp index 2d7b1fd0..1459e07d 100644 --- a/src/nwb/RegisteredType.hpp +++ b/src/nwb/RegisteredType.hpp @@ -238,7 +238,7 @@ class RegisteredType * @return A unique_ptr to the created instance of the subclass. */ inline std::shared_ptr readField( - const std::string& fieldPath) + const std::string& fieldPath) const { return this->create(AQNWB::mergePaths(m_path, fieldPath), m_io); } @@ -336,7 +336,7 @@ class RegisteredType * @brief Defines a lazy-loaded field accessor function. * * This macro generates a function that returns a lazy-loaded wrapper for a - * field. + * dataset or attribute field. * * \note * The Doxyfile.in defines a simplified expansion of this function @@ -370,5 +370,49 @@ class RegisteredType m_io, AQNWB::mergePaths(m_path, fieldPath)); \ } +/** + * @brief Defines a lazy-loaded accessor function for reading fields that are + * RegisteredTypes + * + * This macro generates a function that returns the approbriate subtype of + * RegisteredType, e.g., to read VectorData from a DynamicTable or a + * TimeSeries from and NWBFile. + * + * \note + * The Doxyfile.in defines a simplified expansion of this function + * for generating the documentation for the autogenerated function. + * This means: 1) When updating the macro here, we also need to ensure + * that the expansion in the Doxyfile.in is still accurate and 2) the + * docstring that is defined by the macro here is not being used by + * Doxygen but the version generated by its on PREDEFINED expansion. + * + * @param name The name of the function to generate. + * @param registeredType The specific subclass of registered type to use + * @param fieldPath The path to the field. + * @param description A detailed description of the field. + */ +#define DEFINE_REGISTERED_FIELD(name, registeredType, fieldPath, description) \ + /** \ + * @brief Returns the instance of the class representing the ##name field. \ + * \ + * @tparam RTYPE The RegisteredType of the field (default: ##registeredType) \ + * In most cases this should not be changed. But in the case of templated \ + * types, e.g,. VectorData a user may want to change this to a \ + * more specific subtype to use, e.g., VectorData \ + * @return A shared pointer to an instance of ##registeredType representing \ + * the object. May return nullptr if the path does not exist \ + * \ + * description \ + */ \ + template \ + inline std::shared_ptr name() const \ + { \ + std::string objectPath = AQNWB::mergePaths(m_path, fieldPath); \ + if (m_io->objectExists(objectPath)) { \ + return RegisteredType::create(objectPath, m_io); \ + } \ + return nullptr; \ + } + } // namespace NWB } // namespace AQNWB diff --git a/src/nwb/file/ElectrodeTable.cpp b/src/nwb/file/ElectrodeTable.cpp index 5494bdd7..b7c81f10 100644 --- a/src/nwb/file/ElectrodeTable.cpp +++ b/src/nwb/file/ElectrodeTable.cpp @@ -16,9 +16,9 @@ ElectrodeTable::ElectrodeTable(std::shared_ptr io) {"group", "group_name", "location"}) , m_electrodeDataset(std::make_unique( AQNWB::mergePaths(electrodeTablePath, "id"), io)) - , m_groupNamesDataset(std::make_unique( + , m_groupNamesDataset(std::make_unique>( AQNWB::mergePaths(electrodeTablePath, "group_name"), io)) - , m_locationsDataset(std::make_unique( + , m_locationsDataset(std::make_unique>( AQNWB::mergePaths(electrodeTablePath, "location"), io)) { } @@ -28,9 +28,9 @@ ElectrodeTable::ElectrodeTable(const std::string& path, : DynamicTable(electrodeTablePath, io) , m_electrodeDataset(std::make_unique( AQNWB::mergePaths(electrodeTablePath, "id"), io)) - , m_groupNamesDataset(std::make_unique( + , m_groupNamesDataset(std::make_unique>( AQNWB::mergePaths(electrodeTablePath, "group_name"), io)) - , m_locationsDataset(std::make_unique( + , m_locationsDataset(std::make_unique>( AQNWB::mergePaths(electrodeTablePath, "location"), io)) { std::cerr << "ElectrodeTable object is required to appear at " @@ -46,6 +46,8 @@ void ElectrodeTable::initialize(const std::string& description) { // create group DynamicTable::initialize(description); + IO::BaseDataType vstrType(IO::BaseDataType::Type::V_STR, + 0); // 0 indicates variable length m_electrodeDataset->initialize(std::unique_ptr( m_io->createArrayDataSet(IO::BaseDataType::I32, @@ -54,14 +56,14 @@ void ElectrodeTable::initialize(const std::string& description) AQNWB::mergePaths(m_path, "id")))); m_groupNamesDataset->initialize( std::unique_ptr( - m_io->createArrayDataSet(IO::BaseDataType::STR(250), + m_io->createArrayDataSet(vstrType, SizeArray {0}, SizeArray {1}, AQNWB::mergePaths(m_path, "group_name"))), "the name of the ElectrodeGroup this electrode is a part of"); m_locationsDataset->initialize( std::unique_ptr( - m_io->createArrayDataSet(IO::BaseDataType::STR(250), + m_io->createArrayDataSet(vstrType, SizeArray {0}, SizeArray {1}, AQNWB::mergePaths(m_path, "location"))), diff --git a/src/nwb/file/ElectrodeTable.hpp b/src/nwb/file/ElectrodeTable.hpp index 1676a73a..455211ce 100644 --- a/src/nwb/file/ElectrodeTable.hpp +++ b/src/nwb/file/ElectrodeTable.hpp @@ -81,6 +81,18 @@ class ElectrodeTable : public DynamicTable inline const static std::string electrodeTablePath = "/general/extracellular_ephys/electrodes"; + DEFINE_REGISTERED_FIELD( + readLocationColumn, + VectorData, + "location", + "the location of channel within the subject e.g. brain region") + + DEFINE_REGISTERED_FIELD( + readGroupNameColumn, + VectorData, + "group_name", + "the name of the ElectrodeGroup this electrode is a part of") + private: /** * @brief The global indices for each electrode. @@ -116,11 +128,11 @@ class ElectrodeTable : public DynamicTable /** * @brief The group names column for write */ - std::unique_ptr m_groupNamesDataset; + std::unique_ptr> m_groupNamesDataset; /** * @brief The locations column for write */ - std::unique_ptr m_locationsDataset; + std::unique_ptr> m_locationsDataset; }; } // namespace AQNWB::NWB diff --git a/src/nwb/hdmf/base/Data.cpp b/src/nwb/hdmf/base/Data.cpp index 6708ac6f..bd74edc2 100644 --- a/src/nwb/hdmf/base/Data.cpp +++ b/src/nwb/hdmf/base/Data.cpp @@ -1,21 +1,28 @@ #include "nwb/hdmf/base/Data.hpp" -using namespace AQNWB::NWB; - -// Container -// Initialize the static registered_ member to trigger registration -REGISTER_SUBCLASS_IMPL(Data) - -/** Constructor */ -Data::Data(const std::string& path, std::shared_ptr io) - : RegisteredType(path, io) +namespace AQNWB::NWB { -} -void Data::initialize(std::unique_ptr&& dataset) -{ - m_dataset = std::move(dataset); - // setup common attributes - m_io->createCommonNWBAttributes( - m_path, this->getNamespace(), this->getTypeName()); -} \ No newline at end of file +// Explicitly register the Data specialization with the type registry. +// This ensures that the most generic specialization is available for dynamic +// creation with RegisteredType::create. The REGISTER_SUBCLASS_IMPL macro +// initializes the static member to trigger the registration. +template<> +REGISTER_SUBCLASS_IMPL(Data) + +// Explicitly instantiate the Data template for all common data types. +// This ensures that these specializations are generated by the compiler, +// reducing compile times and ensuring availability throughout the program. +template class Data; +template class Data; +template class Data; +template class Data; +template class Data; +template class Data; +template class Data; +template class Data; +template class Data; +template class Data; +template class Data; +template class Data; +} // namespace AQNWB::NWB diff --git a/src/nwb/hdmf/base/Data.hpp b/src/nwb/hdmf/base/Data.hpp index 936bc78f..96d0e478 100644 --- a/src/nwb/hdmf/base/Data.hpp +++ b/src/nwb/hdmf/base/Data.hpp @@ -9,12 +9,17 @@ namespace AQNWB::NWB { /** * @brief An abstract data type for a dataset. + * @tparam DTYPE The data type of the data managed by Data */ +template class Data : public RegisteredType { public: - // Register Data class as a registered type - REGISTER_SUBCLASS(Data, "hdmf-common") + // Register the Data template class with the type registry for dynamic + // creation. This registration is generic and applies to any specialization of + // the Data template. It allows the system to dynamically create instances of + // Data with different data types. + REGISTER_SUBCLASS_WITH_TYPENAME(Data, "hdmf-common", "Data") /** * @brief Constructor. @@ -22,12 +27,15 @@ class Data : public RegisteredType * @param path The path of the container. * @param io A shared pointer to the IO object. */ - Data(const std::string& path, std::shared_ptr io); + Data(const std::string& path, std::shared_ptr io) + : RegisteredType(path, io) + { + } /** - * @brief Destructor. + * @brief Virtual destructor. */ - ~Data() {} + virtual ~Data() override {} /** * @brief Initialize the dataset for the Data object @@ -37,7 +45,13 @@ class Data : public RegisteredType * * @param dataset The rvalue unique pointer to the BaseRecordingData object */ - void initialize(std::unique_ptr&& dataset); + void initialize(std::unique_ptr&& dataset) + { + m_dataset = std::move(dataset); + // setup common attributes + m_io->createCommonNWBAttributes( + m_path, this->getNamespace(), this->getTypeName()); + } /** * @brief Check whether the m_dataset has been initialized @@ -47,7 +61,7 @@ class Data : public RegisteredType std::unique_ptr m_dataset; // Define the data fields to expose for lazy read access - DEFINE_FIELD(readData, DatasetField, std::any, "", The main data) + DEFINE_FIELD(readData, DatasetField, DTYPE, "", The main data) DEFINE_FIELD(readNeurodataType, AttributeField, diff --git a/src/nwb/hdmf/table/DynamicTable.cpp b/src/nwb/hdmf/table/DynamicTable.cpp index 08f03f6e..799dd0ca 100644 --- a/src/nwb/hdmf/table/DynamicTable.cpp +++ b/src/nwb/hdmf/table/DynamicTable.cpp @@ -30,19 +30,22 @@ void DynamicTable::initialize(const std::string& description) } /** Add column to table */ -void DynamicTable::addColumn(std::unique_ptr& vectorData, - const std::vector& values) +void DynamicTable::addColumn( + std::unique_ptr>& vectorData, + const std::vector& values) { if (!vectorData->isInitialized()) { std::cerr << "VectorData dataset is not initialized" << std::endl; } else { // write in loop because variable length string - for (SizeType i = 0; i < values.size(); i++) - vectorData->m_dataset->writeDataBlock( - std::vector {1}, - std::vector {i}, - IO::BaseDataType::STR(values[i].size() + 1), - values); // TODO - add tests for this + IO::BaseDataType vstrType(IO::BaseDataType::Type::V_STR, + 0); // 0 indicates variable length + // for (SizeType i = 0; i < values.size(); i++) + vectorData->m_dataset->writeDataBlock( + std::vector {values.size()}, + std::vector {0}, + vstrType, // IO::BaseDataType::STR(values[i].size() + 1), + values); // TODO - add tests for this } } @@ -71,7 +74,7 @@ void DynamicTable::addReferenceColumn(const std::string& name, } else { std::string columnPath = AQNWB::mergePaths(m_path, name); m_io->createReferenceDataSet(columnPath, values); - auto refColumn = AQNWB::NWB::VectorData(columnPath, m_io); + auto refColumn = AQNWB::NWB::VectorData(columnPath, m_io); refColumn.initialize(nullptr, // Use nullptr because we only want to create // the attributes but not modify the data colDescription); diff --git a/src/nwb/hdmf/table/DynamicTable.hpp b/src/nwb/hdmf/table/DynamicTable.hpp index e1f42871..50c90109 100644 --- a/src/nwb/hdmf/table/DynamicTable.hpp +++ b/src/nwb/hdmf/table/DynamicTable.hpp @@ -54,7 +54,7 @@ class DynamicTable : public Container * @param vectorData A unique pointer to the `VectorData` dataset. * @param values The vector of string values. */ - void addColumn(std::unique_ptr& vectorData, + void addColumn(std::unique_ptr>& vectorData, const std::vector& values); /** @@ -84,6 +84,29 @@ class DynamicTable : public Container m_colNames = newColNames; } + /** + * @brief Read an arbitrary column of the DyanmicTable + * + * For columns defined in the schema the corresponding DEFINE_REGISTERED_FIELD + * read functions are preferred because they help avoid the need for + * specifying the specific name of the column and data type to use. + * + * @return The VectorData object representing the column or a nullptr if the + * column doesn't exists + */ + template + std::shared_ptr> readColumn(const std::string& colName) + { + std::string columnPath = AQNWB::mergePaths(m_path, colName); + if (m_io->objectExists(columnPath)) { + if (m_io->getStorageObjectType(columnPath) == StorageObjectType::Dataset) + { + return std::make_shared>(columnPath, m_io); + } + } + return nullptr; + } + DEFINE_FIELD(readColNames, AttributeField, std::string, @@ -96,11 +119,11 @@ class DynamicTable : public Container "description", Description of what is in this dynamic table) - DEFINE_FIELD(readId, - DatasetField, - std::any, - "id", - Array of unique identifiers for the rows of this dynamic table) + DEFINE_REGISTERED_FIELD( + readIdColumn, + ElementIdentifiers, + "id", + "unique identifiers for the rows of this dynamic table") protected: /** diff --git a/src/nwb/hdmf/table/ElementIdentifiers.hpp b/src/nwb/hdmf/table/ElementIdentifiers.hpp index 41212b24..a9acf169 100644 --- a/src/nwb/hdmf/table/ElementIdentifiers.hpp +++ b/src/nwb/hdmf/table/ElementIdentifiers.hpp @@ -8,10 +8,10 @@ namespace AQNWB::NWB * @brief A list of unique identifiers for values within a dataset, e.g. rows of * a DynamicTable. */ -class ElementIdentifiers : public Data +class ElementIdentifiers : public Data { public: - // Register Data class as a registered type + // Register ElementIdentifiers class as a registered type REGISTER_SUBCLASS(ElementIdentifiers, "hdmf-common") /** @@ -22,7 +22,9 @@ class ElementIdentifiers : public Data */ ElementIdentifiers(const std::string& path, std::shared_ptr io); - // Define the data fields to expose for lazy read access - DEFINE_FIELD(readData, DatasetField, int, "", The data identifiers) + /** + * @brief Virtual destructor. + */ + virtual ~ElementIdentifiers() override {} }; } // namespace AQNWB::NWB diff --git a/src/nwb/hdmf/table/VectorData.cpp b/src/nwb/hdmf/table/VectorData.cpp index 673ccfaf..6bebf610 100644 --- a/src/nwb/hdmf/table/VectorData.cpp +++ b/src/nwb/hdmf/table/VectorData.cpp @@ -1,21 +1,27 @@ #include "nwb/hdmf/table/VectorData.hpp" -using namespace AQNWB::NWB; - -// Initialize the static registered_ member to trigger registration -REGISTER_SUBCLASS_IMPL(VectorData) - -/** Constructor */ -VectorData::VectorData(const std::string& path, - std::shared_ptr io) - : Data(path, io) +namespace AQNWB::NWB { -} +// Explicitly register the Data specialization with the type registry. +// This ensures that the most generic specialization is available for dynamic +// creation with RegisteredType::create. The REGISTER_SUBCLASS_IMPL macro +// initializes the static member to trigger the registration. +template<> +REGISTER_SUBCLASS_IMPL(VectorData) -void VectorData::initialize( - std::unique_ptr&& dataset, - const std::string& description) -{ - Data::initialize(std::move(dataset)); - m_io->createAttribute(description, m_path, "description"); -} +// Explicitly instantiate the Data template for all common data types. +// This ensures that these specializations are generated by the compiler, +// reducing compile times and ensuring availability throughout the program. +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +template class VectorData; +} // namespace AQNWB::NWB diff --git a/src/nwb/hdmf/table/VectorData.hpp b/src/nwb/hdmf/table/VectorData.hpp index 6fce6342..482cfc56 100644 --- a/src/nwb/hdmf/table/VectorData.hpp +++ b/src/nwb/hdmf/table/VectorData.hpp @@ -10,20 +10,35 @@ namespace AQNWB::NWB { /** * @brief An n-dimensional dataset representing a column of a DynamicTable. + * @tparam DTYPE The data type of the data managed by VectorData */ -class VectorData : public Data +template +class VectorData : public Data { public: - // Register Data class as a registered type - REGISTER_SUBCLASS(VectorData, "hdmf-common") - /** * @brief Constructor. * * @param path The path of the container. * @param io A shared pointer to the IO object. */ - VectorData(const std::string& path, std::shared_ptr io); + VectorData(const std::string& path, std::shared_ptr io) + : Data(path, io) + { + } + + // Register the Data template class with the type registry for dynamic + // creation. This registration is generic and applies to any specialization of + // the Data template. It allows the system to dynamically create instances of + // Data with different data types. + REGISTER_SUBCLASS_WITH_TYPENAME(VectorData, + "hdmf-common", + "VectorData") + + /** + * @brief Virtual destructor. + */ + virtual ~VectorData() override {} /** * @brief Initialize the dataset for the Data object @@ -35,7 +50,15 @@ class VectorData : public Data * @param description The description of the VectorData */ void initialize(std::unique_ptr&& dataset, - const std::string& description); + const std::string& description) + { + Data::initialize(std::move(dataset)); + this->m_io->createAttribute(description, this->m_path, "description"); + } + + // Define the data fields to expose for lazy read access + using RegisteredType::m_io; + using RegisteredType::m_path; DEFINE_FIELD(readDescription, AttributeField, diff --git a/src/nwb/misc/AnnotationSeries.hpp b/src/nwb/misc/AnnotationSeries.hpp index a9ac2ac1..54fa28b1 100644 --- a/src/nwb/misc/AnnotationSeries.hpp +++ b/src/nwb/misc/AnnotationSeries.hpp @@ -34,6 +34,7 @@ class AnnotationSeries : public TimeSeries /** * @brief Initializes the AnnotationSeries * @param description The description of the AnnotationSeries. + * @param comments Comments about the AnnotationSeries. * @param dsetSize Initial size of the main dataset. This must be a vector * with one element specifying the length in time. * @param chunkSize Chunk size to use. diff --git a/tests/examples/test_ecephys_data_read.cpp b/tests/examples/test_ecephys_data_read.cpp index ab791973..0556e85c 100644 --- a/tests/examples/test_ecephys_data_read.cpp +++ b/tests/examples/test_ecephys_data_read.cpp @@ -203,6 +203,22 @@ TEST_CASE("ElectricalSeriesReadExample", "[ecephys]") readio->open(FileMode::ReadOnly); // [example_read_new_io_snippet] + // [example_read_predefined_types] + // Read the NWBFile + auto readNWBFile = + NWB::RegisteredType::create("/", readio); + // Read the ElectrodesTable + auto readElectrodeTable = readNWBFile->readElectrodeTable(); + // read the location data. Note that both the type of the class and + // the data values is being set for us, here, VectorData + auto locationColumn = readElectrodeTable->readLocationColumn(); + auto locationColumnValues = locationColumn->readData()->values(); + // confirm that the values are correct + std::vector expectedLocationValues = { + "unknown", "unknown", "unknown", "unknown"}; + REQUIRE(locationColumnValues.data == expectedLocationValues); + // [example_read_predefined_types] + std::cout << "Searching and reading the ElectricalSeries container" << std::endl; // [example_search_types_snippet] @@ -280,9 +296,6 @@ TEST_CASE("ElectricalSeriesReadExample", "[ecephys]") // [example_read_generic_dataset_field_snippet] // [example_read_generic_registeredtype_field_snippet] - // read the NWBFile - auto readNWBFile = - NWB::RegisteredType::create("/", readio); // read the ElectricalSeries from the NWBFile object via the readField // method returning a generic std::shared_ptr auto readRegisteredType = readNWBFile->readField(esdata_path); diff --git a/tests/testData.cpp b/tests/testData.cpp index 0d4ee18c..d7e68b77 100644 --- a/tests/testData.cpp +++ b/tests/testData.cpp @@ -34,7 +34,7 @@ TEST_CASE("Data", "[base]") io->createArrayDataSet(dataType, dataShape, chunking, dataPath); // setup Data object - NWB::Data columnData = NWB::Data(dataPath, io); + NWB::Data columnData = NWB::Data(dataPath, io); columnData.initialize(std::move(columnDataset)); // Write data to file @@ -49,8 +49,8 @@ TEST_CASE("Data", "[base]") readio->open(FileMode::ReadOnly); // Read all fields using the standard read methods - auto readRegisteredType = NWB::RegisteredType::create(dataPath, readio); - auto readData = std::dynamic_pointer_cast(readRegisteredType); + auto readData = + NWB::RegisteredType::create>(dataPath, readio); // Read the "namespace" attribute via the readNamespace field auto namespaceData = readData->readNamespace(); diff --git a/tests/testFile.cpp b/tests/testFile.cpp index 7e26e9d9..c9f33418 100644 --- a/tests/testFile.cpp +++ b/tests/testFile.cpp @@ -6,13 +6,14 @@ #include "io/hdf5/HDF5IO.hpp" #include "io/hdf5/HDF5RecordingData.hpp" #include "nwb/file/ElectrodeTable.hpp" +#include "nwb/hdmf/table/ElementIdentifiers.hpp" #include "testUtils.hpp" using namespace AQNWB; TEST_CASE("ElectrodeTable", "[ecephys]") { - SECTION("test initialization") + SECTION("test initialization and read") { std::string filename = getTestFilePath("electrodeTable.h5"); std::shared_ptr io = std::make_unique(filename); @@ -44,6 +45,53 @@ TEST_CASE("ElectrodeTable", "[ecephys]") std::vector read_channels(buffer, buffer + numChannels); delete[] buffer; REQUIRE(channelIDs == read_channels); + + // Test reading the location data + auto readLocation = electrodeTable.readLocationColumn(); + REQUIRE(readLocation != nullptr); + auto readLocationData = readLocation->readData(); + auto readLocationValues = readLocationData->values().data; + REQUIRE(readLocationValues.size() == 3); + std::vector expectedLocations = { + "unknown", "unknown", "unknown"}; + REQUIRE(readLocationValues == expectedLocations); + + // Test reading the groupName data + auto readGroupName = electrodeTable.readGroupNameColumn(); + REQUIRE(readGroupName != nullptr); + auto readGroupNameData = readGroupName->readData(); + auto readGroupNameValues = readGroupNameData->values().data; + REQUIRE(readGroupNameValues.size() == 3); + std::vector expectedGroupNames = { + "array0", "array0", "array0"}; + REQUIRE(readGroupNameValues == expectedGroupNames); + + // Test reading the id column + std::shared_ptr readId = + electrodeTable.readIdColumn(); + REQUIRE(readId != nullptr); + auto readIdData = readId->readData(); + auto readIdValues = readIdData->values().data; + REQUIRE(readIdValues.size() == 3); + std::vector expectedIdValues = {0, 1, 2}; + REQUIRE(readIdValues == expectedIdValues); + + // Test reading columns via the generic readColumn method + auto readGroupName2 = electrodeTable.readColumn("group_name"); + REQUIRE(readGroupName2 != nullptr); + auto readGroupNameData2 = readGroupName2->readData(); + auto readGroupNameValues2 = readGroupNameData2->values().data; + REQUIRE(readGroupNameValues.size() == 3); + REQUIRE(readGroupNameValues == expectedGroupNames); + + // Test reading id column via the generic readColumn method as VectorData + std::shared_ptr> readId2 = + electrodeTable.readColumn("id"); + REQUIRE(readId2 != nullptr); + auto readIdData2 = readId2->readData(); + auto readIdValues2 = readIdData2->values().data; + REQUIRE(readIdValues.size() == 3); + REQUIRE(readIdValues == expectedIdValues); } SECTION("test initialization with empty channels") diff --git a/tests/testVectorData.cpp b/tests/testVectorData.cpp index a8ca3061..17972352 100644 --- a/tests/testVectorData.cpp +++ b/tests/testVectorData.cpp @@ -35,7 +35,7 @@ TEST_CASE("VectorData", "[base]") io->createArrayDataSet(dataType, dataShape, chunking, dataPath); // setup VectorData object - NWB::VectorData columnVectorData = NWB::VectorData(dataPath, io); + NWB::VectorData columnVectorData = NWB::VectorData(dataPath, io); columnVectorData.initialize(std::move(columnDataset), description); // Write data to file @@ -52,7 +52,7 @@ TEST_CASE("VectorData", "[base]") // Read all fields using the standard read methods auto readRegisteredType = NWB::RegisteredType::create(dataPath, readio); auto readVectorData = - std::dynamic_pointer_cast(readRegisteredType); + std::make_shared>(dataPath, readio); // Read the "namespace" attribute via the readNamespace field auto namespaceData = readVectorData->readNamespace(); @@ -100,7 +100,8 @@ TEST_CASE("VectorData", "[base]") io->createArrayDataSet(dataType, dataShape, chunking, dataPath); // setup VectorData object - NWB::VectorData columnVectorData = NWB::VectorData(dataPath, io); + NWB::VectorData columnVectorData = + NWB::VectorData(dataPath, io); columnVectorData.initialize(std::move(columnDataset), description); // Write data to file @@ -117,7 +118,7 @@ TEST_CASE("VectorData", "[base]") // Read all fields using the standard read methods auto readRegisteredType = NWB::RegisteredType::create(dataPath, readio); auto readVectorData = - std::dynamic_pointer_cast(readRegisteredType); + std::make_shared>(dataPath, readio); // Read the "namespace" attribute via the readNamespace field auto namespaceData = readVectorData->readNamespace(); @@ -173,7 +174,8 @@ TEST_CASE("VectorData", "[base]") io->createArrayDataSet(dataType, dataShape, chunking, dataPath); // setup VectorData object - NWB::VectorData columnVectorData = NWB::VectorData(dataPath, io); + NWB::VectorData columnVectorData = + NWB::VectorData(dataPath, io); columnVectorData.initialize(std::move(columnDataset), description); // Write data to file @@ -190,7 +192,7 @@ TEST_CASE("VectorData", "[base]") // Read all fields using the standard read methods auto readRegisteredType = NWB::RegisteredType::create(dataPath, readio); auto readVectorData = - std::dynamic_pointer_cast(readRegisteredType); + std::make_shared>(dataPath, readio); // Read the "namespace" attribute via the readNamespace field auto namespaceData = readVectorData->readNamespace();