aqnwb 0.1.0
|
New neurodata_types typically inherit from at least either Container or Data, or a more specialized type of the two. In any case, all classes that represent a neurodata_type
defined in the schema should be implemented as a subtype of RegisteredType.
To implement a subclass of RegisteredType, follow these steps:
RegisteredType.hpp
header file in your subclass header file. (const std::string& path, std::shared_ptr<IO::BaseIO> io)
, as the "create" method expects this constructor signature. hpp
) file as part of the class definition: cpp
) file, initialize the static member to trigger the registration using the REGISTER_SUBCLASS_IMPL macro: 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.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 <v2.7, which did not have an assigned neurodata_type
, but was implemented as a regular DynamicTable
. To allow us to define a class ElectrodeTable
to help with writing the table we can then use REGISTER_SUBCLASS_WITH_TYPENAME(ElectrodeTable, "core", "DynamicTable")
in the ElectrodesTable
class. This ensures that the neurodata_type
attribute is set correctly to DynamicTable
on write instead of ElectrodesTable
. However, on read this will by default not use the ElectrodesTable
class but the regular DynamicTable
class since that is what the schema is indicating to use. In the registry, the class will still be registered under the core::ElectrodesTable
key, but with "DynamicTable" as the typename value and the ElectrodesTable.getTypeName
automatic override returning the indicated typename instead of the classname.DEFINE_FIELD
creates templated, non-virtual read functions. This means if we want to "redefine" a field in a child class by calling DEFINE_FIELD
again, then the function will be "hidden" instead of "override". This is important to remember when casting a pointer to a base type, as in this case the implementation from the base type will be used since the function created by DEFINE_FIELD
is not virtual.MySubClass.hpp
MySubClass.cpp
The type registry in RegisteredType allows for dynamic creation of registered subclasses by name. Here is how it works:
std::unordered_set
to store subclass names (which can be accessed via getRegistry()) and 2) an std::unordered_map
to store factory functions for creating instances of the subclasses (which can be accessed via getFactoryMap()). The factory methods are the required constructor that uses the io and path as input.REGISTER_SUBCLASS
macro, which defines a static method (registerSubclass()
) and static member (registered_
) to trigger the registration when the subclass is loaded.REGISTER_SUBCLASS_IMPL
macro initializes the static member (registered_
), which triggers the registerSubclass method and ensures that the subclass is registered when the program starts.REGISTER_SUBCLASS
macro implements an automatic override of the methods to ensure the appropriate type and namespace string are returned. These methods should, hence, not be manually overridden by subclasses, to ensure consistency in type identification.The RegisteredType registry allows for dynamic creation and management of registered subclasses. Here is how you can use it:
The DEFINE_FIELD macro takes the following main inputs:
name
: The name of the function to generate.storageObjectType
: One of either DatasetField or AttributeField to define the type of storage object used to store the field.default_type
: The default data type to use. If not known, we can use std::any
.fieldPath
: Literal string with the relative path to the field within the schema of the respective neurodata_type. This is automatically being expanded at runtime to the full path.description
: Description of the field to include in the docstring for the docsAll of these inputs are required. A typical example will look as follows:
The compiler will then expand this definition to create a new method called getData
that will return a ReadDataWrapper for lazy reading for the field. The corresponding expanded function will look something like:
See Reading data for an example of how to use such methods (e.g., TimeSeries::readData ) for reading data fields from a file.